From 80b17af40c35eb9e0abecbb68691e002f75eb0ba Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 5 Jul 2021 18:49:45 -0400 Subject: [PATCH 001/412] #47 fixed --- qurator/eynollah/utils/drop_capitals.py | 73 +++++++++++++------------ 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/qurator/eynollah/utils/drop_capitals.py b/qurator/eynollah/utils/drop_capitals.py index a69e9f5..6d1edfa 100644 --- a/qurator/eynollah/utils/drop_capitals.py +++ b/qurator/eynollah/utils/drop_capitals.py @@ -142,53 +142,54 @@ def adhere_drop_capital_region_into_corresponding_textline( # areas_main=np.array([cv2.contourArea(all_found_texline_polygons[int(region_final)][0][j] ) for j in range(len(all_found_texline_polygons[int(region_final)]))]) # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + try: + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # print(all_box_coord[j_cont]) + # print(cx_t) + # print(cy_t) + # print(cx_d[i_drop]) + # print(cy_d[i_drop]) + y_lines = np.array(cy_t) # all_box_coord[int(region_final)][0]+np.array(cy_t) - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) - # print(all_box_coord[j_cont]) - # print(cx_t) - # print(cy_t) - # print(cx_d[i_drop]) - # print(cy_d[i_drop]) - y_lines = np.array(cy_t) # all_box_coord[int(region_final)][0]+np.array(cy_t) + y_lines[y_lines < y_min_d[i_drop]] = 0 + # print(y_lines) - y_lines[y_lines < y_min_d[i_drop]] = 0 - # print(y_lines) + arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) + # print(arg_min) - arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) - # print(arg_min) + cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] + cnt_nearest[:, 0, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] - cnt_nearest[:, 0, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] + img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) + img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) + img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) - img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) - img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) - img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) + img_textlines = img_textlines.astype(np.uint8) - img_textlines = img_textlines.astype(np.uint8) + # plt.imshow(img_textlines) + # plt.show() + imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) - # plt.imshow(img_textlines) - # plt.show() - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + # print(len(contours_combined),'len textlines mixed') + areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) - # print(len(contours_combined),'len textlines mixed') - areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) + contours_biggest = contours_combined[np.argmax(areas_cnt_text)] - contours_biggest = contours_combined[np.argmax(areas_cnt_text)] + # print(np.shape(contours_biggest)) + # print(contours_biggest[:]) + # contours_biggest[:,0,0]=contours_biggest[:,0,0]#-all_box_coord[int(region_final)][2] + # contours_biggest[:,0,1]=contours_biggest[:,0,1]#-all_box_coord[int(region_final)][0] + # print(np.shape(contours_biggest),'contours_biggest') + # print(np.shape(all_found_texline_polygons[int(region_final)][arg_min])) + ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) + all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest + except: + pass - # print(np.shape(contours_biggest)) - # print(contours_biggest[:]) - # contours_biggest[:,0,0]=contours_biggest[:,0,0]#-all_box_coord[int(region_final)][2] - # contours_biggest[:,0,1]=contours_biggest[:,0,1]#-all_box_coord[int(region_final)][0] - # print(np.shape(contours_biggest),'contours_biggest') - # print(np.shape(all_found_texline_polygons[int(region_final)][arg_min])) - ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest - - # print(cx_t,'print') try: # print(all_found_texline_polygons[j_cont][0]) cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) From a5c940705a533ea05d8050167fa4e178095ba36b Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 5 Jul 2021 23:20:55 -0400 Subject: [PATCH 002/412] tables are integrated --- qurator/eynollah/cli.py | 8 + qurator/eynollah/eynollah.py | 486 +++++++++++++++++++++++++++-- qurator/eynollah/utils/__init__.py | 6 +- qurator/eynollah/utils/contour.py | 14 + qurator/eynollah/utils/rotate.py | 9 +- qurator/eynollah/writer.py | 12 +- 6 files changed, 502 insertions(+), 33 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a895b0d..dfe3bc6 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -73,6 +73,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="if this parameter set to true, this tool will try to return all elements of layout.", ) +@click.option( + "--tables/--no-tables", + "-tab/-notab", + is_flag=True, + help="if this parameter set to true, this tool will try to detect tables.", +) @click.option( "--input_binary/--input-RGB", "-ib/-irgb", @@ -109,6 +115,7 @@ def main( allow_enhancement, curved_line, full_layout, + tables, input_binary, allow_scaling, headers_off, @@ -135,6 +142,7 @@ def main( allow_enhancement=allow_enhancement, curved_line=curved_line, full_layout=full_layout, + tables=tables, input_binary=input_binary, allow_scaling=allow_scaling, headers_off=headers_off, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2b8b97e..4a7cb12 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -26,12 +26,15 @@ sys.stderr = stderr import tensorflow as tf tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") - +from scipy.signal import find_peaks +import matplotlib.pyplot as plt from .utils.contour import ( filter_contours_area_of_image, + filter_contours_area_of_image_tables, find_contours_mean_y_diff, find_new_features_of_contours, + find_features_of_contours, get_text_region_boxes_by_given_contours, get_textregion_contours_in_org_image, return_contours_of_image, @@ -92,6 +95,7 @@ class Eynollah: allow_enhancement=False, curved_line=False, full_layout=False, + tables=False, input_binary=False, allow_scaling=False, headers_off=False, @@ -110,6 +114,7 @@ class Eynollah: self.allow_enhancement = allow_enhancement self.curved_line = curved_line self.full_layout = full_layout + self.tables = tables self.input_binary = input_binary self.allow_scaling = allow_scaling self.headers_off = headers_off @@ -137,6 +142,7 @@ class Eynollah: self.model_page_dir = dir_models + "/model_page_mixed_best.h5" self.model_region_dir_p_ens = dir_models + "/model_ensemble_s.h5" self.model_textline_dir = dir_models + "/model_textline_newspapers.h5" + self.model_tables = dir_models + "/model_tables_ens_mixed_new_2.h5" def _cache_images(self, image_filename=None, image_pil=None): ret = {} @@ -1612,11 +1618,309 @@ class Eynollah: order_text_new.append(np.where(np.array(order_of_texts_tot) == iii)[0][0]) return order_text_new, id_of_texts_tot + def check_iou_of_bounding_box_and_contour_for_tables(self, layout, table_prediction_early, pixel_tabel, num_col_classifier): + layout_org = np.copy(layout) + layout_org[:,:,0][layout_org[:,:,0]==pixel_tabel] = 0 + layout = (layout[:,:,0]==pixel_tabel)*1 + layout =np.repeat(layout[:, :, np.newaxis], 3, axis=2) + layout = layout.astype(np.uint8) + imgray = cv2.cvtColor(layout, cv2.COLOR_BGR2GRAY ) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + + contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + + contours_new = [] + for i in range(len(contours)): + x, y, w, h = cv2.boundingRect(contours[i]) + iou = cnt_size[i] /float(w*h) *100 + + if iou<60: + layout_contour = np.zeros((layout_org.shape[0], layout_org.shape[1])) + layout_contour= cv2.fillPoly(layout_contour,pts=[contours[i]] ,color=(1,1,1)) + + + layout_contour_sum = layout_contour.sum(axis=0) + layout_contour_sum_diff = np.diff(layout_contour_sum) + layout_contour_sum_diff= np.abs(layout_contour_sum_diff) + layout_contour_sum_diff_smoothed= gaussian_filter1d(layout_contour_sum_diff, 10) + + peaks, _ = find_peaks(layout_contour_sum_diff_smoothed, height=0) + peaks= peaks[layout_contour_sum_diff_smoothed[peaks]>4] + + for j in range(len(peaks)): + layout_contour[:,peaks[j]-3+1:peaks[j]+1+3] = 0 + + layout_contour=cv2.erode(layout_contour[:,:], KERNEL, iterations=5) + layout_contour=cv2.dilate(layout_contour[:,:], KERNEL, iterations=5) + + layout_contour =np.repeat(layout_contour[:, :, np.newaxis], 3, axis=2) + layout_contour = layout_contour.astype(np.uint8) + + imgray = cv2.cvtColor(layout_contour, cv2.COLOR_BGR2GRAY ) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + + contours_sep, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + for ji in range(len(contours_sep) ): + contours_new.append(contours_sep[ji]) + if num_col_classifier>=2: + only_recent_contour_image = np.zeros((layout.shape[0],layout.shape[1])) + only_recent_contour_image= cv2.fillPoly(only_recent_contour_image,pts=[contours_sep[ji]] ,color=(1,1,1)) + table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] + iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 + + if iou_in>20: + layout_org= cv2.fillPoly(layout_org,pts=[contours_sep[ji]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + else: + pass + else: + + layout_org= cv2.fillPoly(layout_org,pts=[contours_sep[ji]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + + else: + contours_new.append(contours[i]) + if num_col_classifier>=2: + only_recent_contour_image = np.zeros((layout.shape[0],layout.shape[1])) + only_recent_contour_image= cv2.fillPoly(only_recent_contour_image,pts=[contours[i]] ,color=(1,1,1)) + + table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] + iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 + + if iou_in>20: + layout_org= cv2.fillPoly(layout_org,pts=[contours[i]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + else: + pass + else: + layout_org= cv2.fillPoly(layout_org,pts=[contours[i]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + + return layout_org, contours_new + def delete_separator_around(self,spliter_y,peaks_neg,image_by_region, pixel_line, pixel_table): + # format of subboxes: box=[x1, x2 , y1, y2] + pix_del = 100 + if len(image_by_region.shape)==3: + for i in range(len(spliter_y)-1): + for j in range(1,len(peaks_neg[i])-1): + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0]==pixel_line ]=0 + image_by_region[spliter_y[i]:spliter_y[i+1],peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,1]==pixel_line ]=0 + image_by_region[spliter_y[i]:spliter_y[i+1],peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,2]==pixel_line ]=0 + + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0]==pixel_table ]=0 + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,1]==pixel_table ]=0 + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,2]==pixel_table ]=0 + else: + for i in range(len(spliter_y)-1): + for j in range(1,len(peaks_neg[i])-1): + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del]==pixel_line ]=0 + + image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del]==pixel_table ]=0 + return image_by_region + def add_tables_heuristic_to_layout(self, image_regions_eraly_p,boxes, slope_mean_hor, spliter_y,peaks_neg_tot, image_revised, num_col_classifier, min_area, pixel_line): + pixel_table =10 + image_revised_1 = self.delete_separator_around(spliter_y, peaks_neg_tot, image_revised, pixel_line, pixel_table) + img_comm_e = np.zeros(image_revised_1.shape) + img_comm = np.repeat(img_comm_e[:, :, np.newaxis], 3, axis=2) + + for indiv in np.unique(image_revised_1): + image_col=(image_revised_1==indiv)*255 + img_comm_in=np.repeat(image_col[:, :, np.newaxis], 3, axis=2) + img_comm_in=img_comm_in.astype(np.uint8) + + imgray = cv2.cvtColor(img_comm_in, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + contours,hirarchy=cv2.findContours(thresh.copy(), cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + + if indiv==pixel_table: + main_contours = filter_contours_area_of_image_tables(thresh, contours, hirarchy, max_area = 1, min_area = 0.001) + else: + main_contours = filter_contours_area_of_image_tables(thresh, contours, hirarchy, max_area = 1, min_area = min_area) + + img_comm = cv2.fillPoly(img_comm, pts = main_contours, color = (indiv, indiv, indiv)) + img_comm = img_comm.astype(np.uint8) + + if not self.isNaN(slope_mean_hor): + image_revised_last = np.zeros((image_regions_eraly_p.shape[0], image_regions_eraly_p.shape[1],3)) + for i in range(len(boxes)): + image_box=img_comm[int(boxes[i][2]):int(boxes[i][3]),int(boxes[i][0]):int(boxes[i][1]),:] + try: + image_box_tabels_1=(image_box[:,:,0]==pixel_table)*1 + contours_tab,_=return_contours_of_image(image_box_tabels_1) + contours_tab=filter_contours_area_of_image_tables(image_box_tabels_1,contours_tab,_,1,0.003) + image_box_tabels_1=(image_box[:,:,0]==pixel_line)*1 + + image_box_tabels_and_m_text=( (image_box[:,:,0]==pixel_table) | (image_box[:,:,0]==1) )*1 + image_box_tabels_and_m_text=image_box_tabels_and_m_text.astype(np.uint8) + + image_box_tabels_1=image_box_tabels_1.astype(np.uint8) + image_box_tabels_1 = cv2.dilate(image_box_tabels_1,KERNEL,iterations = 5) + + contours_table_m_text,_=return_contours_of_image(image_box_tabels_and_m_text) + image_box_tabels=np.repeat(image_box_tabels_1[:, :, np.newaxis], 3, axis=2) + + image_box_tabels=image_box_tabels.astype(np.uint8) + imgray = cv2.cvtColor(image_box_tabels, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + contours_line,hierachy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) + + y_min_main_line ,y_max_main_line=find_features_of_contours(contours_line) + y_min_main_tab ,y_max_main_tab=find_features_of_contours(contours_tab) + + cx_tab_m_text,cy_tab_m_text ,x_min_tab_m_text , x_max_tab_m_text, y_min_tab_m_text ,y_max_tab_m_text, _= find_new_features_of_contours(contours_table_m_text) + cx_tabl,cy_tabl ,x_min_tabl , x_max_tabl, y_min_tabl ,y_max_tabl,_= find_new_features_of_contours(contours_tab) + + if len(y_min_main_tab )>0: + y_down_tabs=[] + y_up_tabs=[] + + for i_t in range(len(y_min_main_tab )): + y_down_tab=[] + y_up_tab=[] + for i_l in range(len(y_min_main_line)): + if y_min_main_tab[i_t]>y_min_main_line[i_l] and y_max_main_tab[i_t]>y_min_main_line[i_l] and y_min_main_tab[i_t]>y_max_main_line[i_l] and y_max_main_tab[i_t]>y_min_main_line[i_l]: + pass + elif y_min_main_tab[i_t]0: + for ijv in range(len(y_min_tab_col1)): + image_revised_last[int(y_min_tab_col1[ijv]):int(y_max_tab_col1[ijv]),:,:]=pixel_table + return image_revised_last def do_order_of_regions(self, *args, **kwargs): if self.full_layout: return self.do_order_of_regions_full_layout(*args, **kwargs) return self.do_order_of_regions_no_full_layout(*args, **kwargs) + + def get_tables_from_model(self, img, num_col_classifier): + img_org = np.copy(img) + + img_height_h = img_org.shape[0] + img_width_h = img_org.shape[1] + + model_region, session_region = self.start_new_session_and_model(self.model_tables) + + patches = False + + if num_col_classifier < 4 and num_col_classifier > 2: + prediction_table = self.do_prediction(patches, img, model_region) + elif num_col_classifier ==2: + height_ext = 0#int( img.shape[0]/4. ) + h_start = int(height_ext/2.) + width_ext = int( img.shape[1]/8. ) + w_start = int(width_ext/2.) + + height_new = img.shape[0]+height_ext + width_new = img.shape[1]+width_ext + + img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 + img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] + + prediction_ext = self.do_prediction(patches, img_new, model_region) + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + + prediction_table = prediction_table.astype(np.int16) + + elif num_col_classifier ==1: + height_ext = 0# int( img.shape[0]/4. ) + h_start = int(height_ext/2.) + width_ext = int( img.shape[1]/4. ) + w_start = int(width_ext/2.) + + height_new = img.shape[0]+height_ext + width_new = img.shape[1]+width_ext + + img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 + img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] + + prediction_ext = self.do_prediction(patches, img_new, model_region) + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table = prediction_table.astype(np.int16) + + elif num_col_classifier ==60: + prediction_table = np.zeros(img.shape) + img_w_half = int(img.shape[1]/2.) + img_h_half = int(img.shape[0]/2.) + + pre1 = self.do_prediction(patches, img[0:img_h_half,0:img_w_half,:], model_region) + pre2 = self.do_prediction(patches, img[0:img_h_half,img_w_half:,:], model_region) + + pre3 = self.do_prediction(patches, img[img_h_half:,0:img_w_half,:], model_region) + pre4 = self.do_prediction(patches, img[img_h_half:,img_w_half:,:], model_region) + + prediction_table[0:img_h_half,0:img_w_half,:] = pre1[:,:,:] + prediction_table[0:img_h_half,img_w_half:,:] = pre2[:,:,:] + + prediction_table[img_h_half:,0:img_w_half,:] = pre3[:,:,:] + prediction_table[img_h_half:,img_w_half:,:] = pre4[:,:,:] + prediction_table = prediction_table.astype(np.int16) + else: + prediction_table = np.zeros(img.shape) + img_w_half = int(img.shape[1]/2.) + + pre1 = self.do_prediction(patches, img[:,0:img_w_half,:], model_region) + pre2 = self.do_prediction(patches, img[:,img_w_half:,:], model_region) + + pre_full = self.do_prediction(patches, img[:,:,:], model_region) + + + prediction_table_full_erode = cv2.erode(pre_full[:,:,0], KERNEL, iterations=4) + prediction_table_full_erode = cv2.dilate(prediction_table_full_erode, KERNEL, iterations=4) + + prediction_table[:,0:img_w_half,:] = pre1[:,:,:] + prediction_table[:,img_w_half:,:] = pre2[:,:,:] + + prediction_table[:,:,0][prediction_table_full_erode[:,:]==1]=1 + prediction_table = prediction_table.astype(np.int16) + + #prediction_table_erode = cv2.erode(prediction_table[:,:,0], self.kernel, iterations=6) + #prediction_table_erode = cv2.dilate(prediction_table_erode, self.kernel, iterations=6) + + prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) + prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) + + del model_region + del session_region + gc.collect() + + + return prediction_table_erode.astype(np.int16) def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): img_g = self.imread(grayscale=True, uint8=True) @@ -1628,6 +1932,12 @@ class Eynollah: img_g3[:, :, 2] = img_g[:, :] image_page, page_coord, cont_page = self.extract_page() + + if self.tables: + table_prediction = self.get_tables_from_model(image_page, num_col_classifier) + else: + table_prediction = (np.zeros((image_page.shape[0], image_page.shape[1]))).astype(np.int16) + if self.plotter: self.plotter.save_page_image(image_page) @@ -1655,7 +1965,7 @@ class Eynollah: except Exception as why: self.logger.error(why) num_col = None - return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page + return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction def run_enhancement(self): self.logger.info("resize and enhance image") @@ -1699,7 +2009,7 @@ class Eynollah: self.logger.info("slope_deskew: %s", slope_deskew) return slope_deskew, slope_first - def run_marginals(self, image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1): + def run_marginals(self, image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction): image_page_rotated, textline_mask_tot = image_page[:, :], textline_mask_tot_ea[:, :] textline_mask_tot[mask_images[:, :] == 1] = 0 @@ -1710,6 +2020,8 @@ class Eynollah: if num_col_classifier in (1, 2): try: regions_without_separators = (text_regions_p[:, :] == 1) * 1 + if self.tables: + regions_without_separators[table_prediction==1] = 1 regions_without_separators = regions_without_separators.astype(np.uint8) text_regions_p = get_marginals(rotate_image(regions_without_separators, slope_deskew), text_regions_p, num_col_classifier, slope_deskew, kernel=KERNEL) except Exception as e: @@ -1720,14 +2032,19 @@ class Eynollah: self.plotter.save_plot_of_layout_main(text_regions_p, image_page) return textline_mask_tot, text_regions_p, image_page_rotated - def run_boxes_no_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, erosion_hurts): + def run_boxes_no_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts): self.logger.debug('enter run_boxes_no_full_layout') if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, textline_mask_tot_d, text_regions_p_1_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, slope_deskew) + _, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) + table_prediction_n = resize_image(table_prediction_n, text_regions_p.shape[0], text_regions_p.shape[1]) regions_without_separators_d = (text_regions_p_1_n[:, :] == 1) * 1 + if self.tables: + regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 regions_without_separators = (text_regions_p[:, :] == 1) * 1 # ( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) + if self.tables: + regions_without_separators[table_prediction ==1 ] = 1 if np.abs(slope_deskew) < SLOPE_THRESHOLD: text_regions_p_1_n = None textline_mask_tot_d = None @@ -1751,26 +2068,148 @@ class Eynollah: regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) + + text_regions_p_tables = np.copy(text_regions_p) + text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) + img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) else: - boxes_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) + + text_regions_p_tables = np.copy(text_regions_p_1_n) + text_regions_p_tables =np.round(text_regions_p_tables) + text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 + + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) + + img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) + img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) + img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) + img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) self.logger.info("detecting boxes took %ss", str(time.time() - t1)) - img_revised_tab = text_regions_p[:, :] + + if self.tables: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + img_revised_tab = np.copy(img_revised_tab2[:,:,0]) + else: + img_revised_tab = np.copy(text_regions_p[:,:]) + img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 + img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 + + text_regions_p[:,:][text_regions_p[:,:]==10] = 0 + text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 + else: + img_revised_tab=text_regions_p[:,:] + #img_revised_tab = text_regions_p[:, :] polygons_of_images = return_contours_of_interested_region(img_revised_tab, 2) - # plt.imshow(img_revised_tab) - # plt.show() + pixel_img = 4 + min_area_mar = 0.00001 + polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + + pixel_img = 10 + contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + + K.clear_session() self.logger.debug('exit run_boxes_no_full_layout') - return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d + return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables - def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions): + def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts): self.logger.debug('enter run_boxes_full_layout') + + if self.tables: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + + text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) + textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) + table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) + + regions_without_seperators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_seperators_d[table_prediction_n[:,:] == 1] = 1 + + regions_without_seperators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_seperators[table_prediction == 1] = 1 + + pixel_lines=3 + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier,pixel_lines) + K.clear_session() + gc.collect() + + if num_col_classifier>=3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_seperators = regions_without_seperators.astype(np.uint8) + regions_without_seperators = cv2.erode(regions_without_seperators[:,:], KERNEL, iterations=6) + + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + regions_without_seperators_d = regions_without_seperators_d.astype(np.uint8) + regions_without_seperators_d = cv2.erode(regions_without_seperators_d[:,:], KERNEL, iterations=6) + else: + pass + + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_seperators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + text_regions_p_tables = np.copy(text_regions_p) + text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) + + img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) + + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_seperators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + text_regions_p_tables = np.copy(text_regions_p_1_n) + text_regions_p_tables = np.round(text_regions_p_tables) + text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 + + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction_n, 10, num_col_classifier) + img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) + + + img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) + img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) + + img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) + + + if np.abs(slope_deskew) < 0.13: + img_revised_tab = np.copy(img_revised_tab2[:,:,0]) + else: + img_revised_tab = np.copy(text_regions_p[:,:]) + img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 + img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 + + + ##img_revised_tab=img_revised_tab2[:,:,0] + #img_revised_tab=text_regions_p[:,:] + text_regions_p[:,:][text_regions_p[:,:]==10] = 0 + text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 + #img_revised_tab[img_revised_tab2[:,:,0]==10] =10 + + pixel_img = 4 + min_area_mar = 0.00001 + polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + + pixel_img = 10 + contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + # set first model with second model text_regions_p[:, :][text_regions_p[:, :] == 2] = 5 text_regions_p[:, :][text_regions_p[:, :] == 3] = 6 @@ -1830,7 +2269,7 @@ class Eynollah: img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) self.logger.debug('exit run_boxes_full_layout') - return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators + return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables def run(self): """ @@ -1848,7 +2287,7 @@ class Eynollah: self.logger.info("Textregion detection took %ss ", str(time.time() - t1)) t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page = \ + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) self.logger.info("Graphics detection took %ss ", str(time.time() - t1)) self.logger.info('cont_page %s', cont_page) @@ -1868,19 +2307,15 @@ class Eynollah: self.logger.info("deskewing took %ss", str(time.time() - t1)) t1 = time.time() - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1) + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) self.logger.info("detection of marginals took %ss", str(time.time() - t1)) t1 = time.time() if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, erosion_hurts) - - pixel_img = 4 - min_area_mar = 0.00001 - polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) if self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions) + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: @@ -2018,7 +2453,6 @@ class Eynollah: K.clear_session() - polygons_of_tabels = [] pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) all_found_texline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) @@ -2058,9 +2492,9 @@ class Eynollah: regions_without_separators_d[(random_pixels_for_image[:, :] == 1) & (text_regions_p_1_n[:, :] == 5)] = 1 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) else: - boxes_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) @@ -2071,7 +2505,7 @@ class Eynollah: else: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, polygons_of_tabels, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %ss", str(time.time() - t0)) return pcgts else: @@ -2081,6 +2515,6 @@ class Eynollah: else: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %ss", str(time.time() - t0)) return pcgts diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index fb6b476..35c9201 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -1585,7 +1585,7 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, pixel_l def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts): boxes=[] - + peaks_neg_tot_tables = [] for i in range(len(splitter_y_new)-1): #print(splitter_y_new[i],splitter_y_new[i+1]) @@ -1679,6 +1679,8 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho peaks_neg_tot=return_points_with_boundies(peaks_neg_fin,0, regions_without_separators[:,:].shape[1]) + peaks_neg_tot_tables.append(peaks_neg_tot) + reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) @@ -2237,4 +2239,4 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho #else: #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) - return boxes + return boxes, peaks_neg_tot_tables diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index 3209731..6b81391 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -109,7 +109,21 @@ def find_new_features_of_contours(contours_main): # dis_x=np.abs(x_max_main-x_min_main) return cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin +def find_features_of_contours(contours_main): + + areas_main=np.array([cv2.contourArea(contours_main[j]) for j in range(len(contours_main))]) + M_main=[cv2.moments(contours_main[j]) for j in range(len(contours_main))] + cx_main=[(M_main[j]['m10']/(M_main[j]['m00']+1e-32)) for j in range(len(M_main))] + cy_main=[(M_main[j]['m01']/(M_main[j]['m00']+1e-32)) for j in range(len(M_main))] + x_min_main=np.array([np.min(contours_main[j][:,0,0]) for j in range(len(contours_main))]) + x_max_main=np.array([np.max(contours_main[j][:,0,0]) for j in range(len(contours_main))]) + + y_min_main=np.array([np.min(contours_main[j][:,0,1]) for j in range(len(contours_main))]) + y_max_main=np.array([np.max(contours_main[j][:,0,1]) for j in range(len(contours_main))]) + + + return y_min_main, y_max_main def return_parent_contours(contours, hierarchy): contours_parent = [contours[i] for i in range(len(contours)) if hierarchy[0][i][3] == -1] return contours_parent diff --git a/qurator/eynollah/utils/rotate.py b/qurator/eynollah/utils/rotate.py index 9cadd4b..603c2d9 100644 --- a/qurator/eynollah/utils/rotate.py +++ b/qurator/eynollah/utils/rotate.py @@ -52,20 +52,21 @@ def rotate_image_different( img, slope): img_rotation = cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows)) return img_rotation -def rotate_max_area(image, rotated, rotated_textline, rotated_layout, angle): +def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_table_prediction, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) h, w, _ = rotated.shape y1 = h // 2 - int(hr / 2) y2 = y1 + int(hr) x1 = w // 2 - int(wr / 2) x2 = x1 + int(wr) - return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2] + return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_table_prediction[y1:y2, x1:x2] -def rotation_not_90_func(img, textline, text_regions_p_1, thetha): +def rotation_not_90_func(img, textline, text_regions_p_1, table_prediction, thetha): rotated = imutils.rotate(img, thetha) rotated_textline = imutils.rotate(textline, thetha) rotated_layout = imutils.rotate(text_regions_p_1, thetha) - return rotate_max_area(img, rotated, rotated_textline, rotated_layout, thetha) + rotated_table_prediction = imutils.rotate(table_prediction, thetha) + return rotate_max_area(img, rotated, rotated_textline, rotated_layout, rotated_table_prediction, thetha) def rotation_not_90_func_full_layout(img, textline, text_regions_p_1, text_regions_p_fully, thetha): rotated = imutils.rotate(img, thetha) diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index 8dfd2b2..3e006e5 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -141,7 +141,7 @@ class EynollahXmlWriter(): with open(out_fname, 'w') as f: f.write(to_xml(pcgts)) - def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_texline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml): + def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_texline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables): self.logger.debug('enter build_pagexml_no_full_layout') # create the file structure @@ -189,6 +189,16 @@ class EynollahXmlWriter(): points_co += str(int((polygons_lines_to_be_written_in_xml[mm][lmm,0,1] ) / self.scale_y)) points_co += ' ' sep_hor.get_Coords().set_points(points_co[:-1]) + for mm in range(len(found_polygons_tables)): + tab_region = TableRegionType(id=counter.next_region_id, Coords=CoordsType()) + page.add_TableRegion(tab_region) + points_co = '' + for lmm in range(len(found_polygons_tables[mm])): + points_co += str(int((found_polygons_tables[mm][lmm,0,0] ) / self.scale_x)) + points_co += ',' + points_co += str(int((found_polygons_tables[mm][lmm,0,1] ) / self.scale_y)) + points_co += ' ' + tab_region.get_Coords().set_points(points_co[:-1]) return pcgts From c67e1554312a69e2cc286156ced60e755eb58422 Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 9 Jul 2021 10:23:45 -0400 Subject: [PATCH 003/412] table detection completed, enhanced images can be now written to output --- qurator/eynollah/cli.py | 8 ++--- qurator/eynollah/eynollah.py | 68 +++++++++++++++++++++++------------- qurator/eynollah/plot.py | 6 +++- qurator/eynollah/writer.py | 4 +-- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index dfe3bc6..5837255 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -124,11 +124,11 @@ def main( if log_level: setOverrideLogLevel(log_level) initLogging() - if not enable_plotting and (save_layout or save_deskewed or save_all or save_images): - print("Error: You used one of -sl, -sd, -sa or -si but did not enable plotting with -ep") + if not enable_plotting and (save_layout or save_deskewed or save_all or save_images or allow_enhancement): + print("Error: You used one of -sl, -sd, -sa or -si or -ae but did not enable plotting with -ep") sys.exit(1) - elif enable_plotting and not (save_layout or save_deskewed or save_all or save_images): - print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa or -si") + elif enable_plotting and not (save_layout or save_deskewed or save_all or save_images or allow_enhancement): + print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa or -si or -ae") sys.exit(1) eynollah = Eynollah( image_filename=image, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 4a7cb12..8afe44a 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -28,6 +28,7 @@ tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") from scipy.signal import find_peaks import matplotlib.pyplot as plt +from scipy.ndimage import gaussian_filter1d from .utils.contour import ( filter_contours_area_of_image, @@ -119,6 +120,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.plotter = None if not enable_plotting else EynollahPlotter( + dir_out=self.dir_out, dir_of_all=dir_of_all, dir_of_deskewed=dir_of_deskewed, dir_of_cropped_images=dir_of_cropped_images, @@ -1636,7 +1638,7 @@ class Eynollah: x, y, w, h = cv2.boundingRect(contours[i]) iou = cnt_size[i] /float(w*h) *100 - if iou<60: + if iou<80: layout_contour = np.zeros((layout_org.shape[0], layout_org.shape[1])) layout_contour= cv2.fillPoly(layout_contour,pts=[contours[i]] ,color=(1,1,1)) @@ -1670,8 +1672,9 @@ class Eynollah: only_recent_contour_image= cv2.fillPoly(only_recent_contour_image,pts=[contours_sep[ji]] ,color=(1,1,1)) table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 + #print(iou_in,'iou_in_in1') - if iou_in>20: + if iou_in>30: layout_org= cv2.fillPoly(layout_org,pts=[contours_sep[ji]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) else: pass @@ -1687,8 +1690,8 @@ class Eynollah: table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 - - if iou_in>20: + #print(iou_in,'iou_in') + if iou_in>30: layout_org= cv2.fillPoly(layout_org,pts=[contours[i]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) else: pass @@ -1719,6 +1722,13 @@ class Eynollah: def add_tables_heuristic_to_layout(self, image_regions_eraly_p,boxes, slope_mean_hor, spliter_y,peaks_neg_tot, image_revised, num_col_classifier, min_area, pixel_line): pixel_table =10 image_revised_1 = self.delete_separator_around(spliter_y, peaks_neg_tot, image_revised, pixel_line, pixel_table) + + try: + image_revised_1[:,:30][image_revised_1[:,:30]==pixel_line] = 0 + image_revised_1[:,image_revised_1.shape[1]-30:][image_revised_1[:,image_revised_1.shape[1]-30:]==pixel_line] = 0 + except: + pass + img_comm_e = np.zeros(image_revised_1.shape) img_comm = np.repeat(img_comm_e[:, :, np.newaxis], 3, axis=2) @@ -1840,6 +1850,12 @@ class Eynollah: if num_col_classifier < 4 and num_col_classifier > 2: prediction_table = self.do_prediction(patches, img, model_region) + pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), model_region) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table[:,:,0][pre_updown[:,:,0]==1]=1 + prediction_table = prediction_table.astype(np.int16) + elif num_col_classifier ==2: height_ext = 0#int( img.shape[0]/4. ) h_start = int(height_ext/2.) @@ -1853,8 +1869,14 @@ class Eynollah: img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] prediction_ext = self.do_prediction(patches, img_new, model_region) - prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + + prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 prediction_table = prediction_table.astype(np.int16) elif num_col_classifier ==1: @@ -1870,26 +1892,16 @@ class Eynollah: img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] prediction_ext = self.do_prediction(patches, img_new, model_region) + + pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) + pre_updown = cv2.flip(pre_updown, -1) + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + + prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 prediction_table = prediction_table.astype(np.int16) - elif num_col_classifier ==60: - prediction_table = np.zeros(img.shape) - img_w_half = int(img.shape[1]/2.) - img_h_half = int(img.shape[0]/2.) - - pre1 = self.do_prediction(patches, img[0:img_h_half,0:img_w_half,:], model_region) - pre2 = self.do_prediction(patches, img[0:img_h_half,img_w_half:,:], model_region) - - pre3 = self.do_prediction(patches, img[img_h_half:,0:img_w_half,:], model_region) - pre4 = self.do_prediction(patches, img[img_h_half:,img_w_half:,:], model_region) - - prediction_table[0:img_h_half,0:img_w_half,:] = pre1[:,:,:] - prediction_table[0:img_h_half,img_w_half:,:] = pre2[:,:,:] - - prediction_table[img_h_half:,0:img_w_half,:] = pre3[:,:,:] - prediction_table[img_h_half:,img_w_half:,:] = pre4[:,:,:] - prediction_table = prediction_table.astype(np.int16) else: prediction_table = np.zeros(img.shape) img_w_half = int(img.shape[1]/2.) @@ -1898,15 +1910,21 @@ class Eynollah: pre2 = self.do_prediction(patches, img[:,img_w_half:,:], model_region) pre_full = self.do_prediction(patches, img[:,:,:], model_region) - + + pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), model_region) + pre_updown = cv2.flip(pre_updown, -1) prediction_table_full_erode = cv2.erode(pre_full[:,:,0], KERNEL, iterations=4) prediction_table_full_erode = cv2.dilate(prediction_table_full_erode, KERNEL, iterations=4) + + prediction_table_full_updown_erode = cv2.erode(pre_updown[:,:,0], KERNEL, iterations=4) + prediction_table_full_updown_erode = cv2.dilate(prediction_table_full_updown_erode, KERNEL, iterations=4) prediction_table[:,0:img_w_half,:] = pre1[:,:,:] prediction_table[:,img_w_half:,:] = pre2[:,:,:] prediction_table[:,:,0][prediction_table_full_erode[:,:]==1]=1 + prediction_table[:,:,0][prediction_table_full_updown_erode[:,:]==1]=1 prediction_table = prediction_table.astype(np.int16) #prediction_table_erode = cv2.erode(prediction_table[:,:,0], self.kernel, iterations=6) @@ -1977,6 +1995,8 @@ class Eynollah: if self.allow_enhancement: img_res = img_res.astype(np.uint8) self.get_image_and_scales(img_org, img_res, scale) + if self.plotter: + self.plotter.save_enhanced_image(img_res) else: self.get_image_and_scales_after_enhancing(img_org, img_res) else: @@ -2100,6 +2120,7 @@ class Eynollah: if self.tables: if np.abs(slope_deskew) < SLOPE_THRESHOLD: img_revised_tab = np.copy(img_revised_tab2[:,:,0]) + img_revised_tab[:,:][(text_regions_p[:,:] == 1) & (img_revised_tab[:,:] != 10)] = 1 else: img_revised_tab = np.copy(text_regions_p[:,:]) img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 @@ -2310,7 +2331,6 @@ class Eynollah: textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) self.logger.info("detection of marginals took %ss", str(time.time() - t1)) t1 = time.time() - if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) diff --git a/qurator/eynollah/plot.py b/qurator/eynollah/plot.py index 18a7c14..b22c8f1 100644 --- a/qurator/eynollah/plot.py +++ b/qurator/eynollah/plot.py @@ -17,6 +17,7 @@ class EynollahPlotter(): def __init__( self, *, + dir_out, dir_of_all, dir_of_deskewed, dir_of_layout, @@ -26,6 +27,7 @@ class EynollahPlotter(): scale_x=1, scale_y=1, ): + self.dir_out = dir_out self.dir_of_all = dir_of_all self.dir_of_layout = dir_of_layout self.dir_of_cropped_images = dir_of_cropped_images @@ -125,7 +127,9 @@ class EynollahPlotter(): def save_page_image(self, image_page): if self.dir_of_all is not None: cv2.imwrite(os.path.join(self.dir_of_all, self.image_filename_stem + "_page.png"), image_page) - + def save_enhanced_image(self, img_res): + cv2.imwrite(os.path.join(self.dir_out, self.image_filename_stem + "_enhanced.png"), img_res) + def save_plot_of_textline_density(self, img_patch_org): if self.dir_of_all is not None: plt.figure(figsize=(80,40)) diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index 3e006e5..2bacb17 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -194,9 +194,9 @@ class EynollahXmlWriter(): page.add_TableRegion(tab_region) points_co = '' for lmm in range(len(found_polygons_tables[mm])): - points_co += str(int((found_polygons_tables[mm][lmm,0,0] ) / self.scale_x)) + points_co += str(int((found_polygons_tables[mm][lmm,0,0] + page_coord[2]) / self.scale_x)) points_co += ',' - points_co += str(int((found_polygons_tables[mm][lmm,0,1] ) / self.scale_y)) + points_co += str(int((found_polygons_tables[mm][lmm,0,1] + page_coord[0]) / self.scale_y)) points_co += ' ' tab_region.get_Coords().set_points(points_co[:-1]) From b3b49272a5bbd51e46629f6d6231c767100597e3 Mon Sep 17 00:00:00 2001 From: vahid Date: Sat, 10 Jul 2021 07:28:31 -0400 Subject: [PATCH 004/412] README is updated --- README.md | 10 +++++++++- qurator/eynollah/cli.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f521c9d..238fc57 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,18 @@ eynollah \ -o \ -m \ -fl \ --ae \ +-ae \ -as \ -cl \ -si +-sd +-sa +-tab +-ib +-ho +-sl +-ep + ``` The tool does accept and works better on original images (RGB format) than binarized images. diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 5837255..f343918 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -125,10 +125,10 @@ def main( setOverrideLogLevel(log_level) initLogging() if not enable_plotting and (save_layout or save_deskewed or save_all or save_images or allow_enhancement): - print("Error: You used one of -sl, -sd, -sa or -si or -ae but did not enable plotting with -ep") + print("Error: You used one of -sl, -sd, -sa, -si or -ae but did not enable plotting with -ep") sys.exit(1) elif enable_plotting and not (save_layout or save_deskewed or save_all or save_images or allow_enhancement): - print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa or -si or -ae") + print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa, -si or -ae") sys.exit(1) eynollah = Eynollah( image_filename=image, From 9f64110513e0d9dbc625ba8a1400088d46979b89 Mon Sep 17 00:00:00 2001 From: vahid Date: Sat, 10 Jul 2021 07:31:15 -0400 Subject: [PATCH 005/412] README is updated --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 238fc57..c52560b 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,13 @@ eynollah \ -ae \ -as \ -cl \ --si --sd --sa --tab --ib --ho --sl +-si \ +-sd \ +-sa \ +-tab \ +-ib \ +-ho \ +-sl \ -ep ``` From 254abf4d3d8a9d49abb8908e3173afe0ba6285f6 Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 12 Jul 2021 12:02:17 -0400 Subject: [PATCH 006/412] more modifications for tables --- qurator/eynollah/eynollah.py | 110 +++++++++++++++++------------ qurator/eynollah/utils/__init__.py | 39 ++++++---- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8afe44a..ea4a60b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1174,7 +1174,7 @@ class Eynollah: try: img_only_regions = cv2.erode(img_only_regions_with_sep[:,:], KERNEL, iterations=20) - _, _ = find_num_col(img_only_regions, multiplier=6.0) + _, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1]*(1.2 if is_image_enhanced else 1))) @@ -1976,7 +1976,7 @@ class Eynollah: try: - num_col, _ = find_num_col(img_only_regions, multiplier=6.0) + num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) num_col = num_col + 1 if not num_column_is_classified: num_col_classifier = num_col + 1 @@ -2071,10 +2071,10 @@ class Eynollah: regions_without_separators_d = None pixel_lines = 3 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - _, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + _, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) K.clear_session() self.logger.info("num_col_classifier: %s", num_col_classifier) @@ -2088,7 +2088,7 @@ class Eynollah: regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) @@ -2098,7 +2098,7 @@ class Eynollah: img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) @@ -2156,34 +2156,34 @@ class Eynollah: textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) - regions_without_seperators_d=(text_regions_p_1_n[:,:] == 1)*1 - regions_without_seperators_d[table_prediction_n[:,:] == 1] = 1 + regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 - regions_without_seperators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) - regions_without_seperators[table_prediction == 1] = 1 + regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators[table_prediction == 1] = 1 pixel_lines=3 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier,pixel_lines) + num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) K.clear_session() gc.collect() if num_col_classifier>=3: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_seperators = regions_without_seperators.astype(np.uint8) - regions_without_seperators = cv2.erode(regions_without_seperators[:,:], KERNEL, iterations=6) + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:,:], KERNEL, iterations=6) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - regions_without_seperators_d = regions_without_seperators_d.astype(np.uint8) - regions_without_seperators_d = cv2.erode(regions_without_seperators_d[:,:], KERNEL, iterations=6) + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:,:], KERNEL, iterations=6) else: pass if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_seperators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) text_regions_p_tables = np.copy(text_regions_p) text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 pixel_line = 3 @@ -2192,7 +2192,7 @@ class Eynollah: img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_seperators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) text_regions_p_tables = np.copy(text_regions_p_1_n) text_regions_p_tables = np.round(text_regions_p_tables) text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 @@ -2271,20 +2271,20 @@ class Eynollah: text_regions_p[:, :][regions_fully_np[:, :, 0] == 4] = 4 #plt.imshow(text_regions_p) #plt.show() + if not self.tables: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout(image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout(image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) + text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) + textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) + regions_fully_n = resize_image(regions_fully_n, text_regions_p.shape[0], text_regions_p.shape[1]) + regions_without_separators_d = (text_regions_p_1_n[:, :] == 1) * 1 + else: + text_regions_p_1_n = None + textline_mask_tot_d = None + regions_without_separators_d = None - text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) - textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) - regions_fully_n = resize_image(regions_fully_n, text_regions_p.shape[0], text_regions_p.shape[1]) - regions_without_separators_d = (text_regions_p_1_n[:, :] == 1) * 1 - else: - text_regions_p_1_n = None - textline_mask_tot_d = None - regions_without_separators_d = None - - regions_without_separators = (text_regions_p[:, :] == 1) * 1 # ( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators = (text_regions_p[:, :] == 1) * 1 K.clear_session() img_revised_tab = np.copy(text_regions_p[:, :]) @@ -2327,6 +2327,8 @@ class Eynollah: slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) self.logger.info("deskewing took %ss", str(time.time() - t1)) t1 = time.time() + #plt.imshow(table_prediction) + #plt.show() textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) self.logger.info("detection of marginals took %ss", str(time.time() - t1)) @@ -2482,14 +2484,14 @@ class Eynollah: if not self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines, contours_only_text_parent_h) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines, contours_only_text_parent_h_d_ordered) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) elif self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, pixel_lines) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) # print(peaks_neg_fin,peaks_neg_fin_d,'num_col2') # print(splitter_y_new,splitter_y_new_d,'num_col_classifier') @@ -2499,22 +2501,42 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: regions_without_separators = regions_without_separators.astype(np.uint8) regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - random_pixels_for_image = np.random.randn(regions_without_separators.shape[0], regions_without_separators.shape[1]) - random_pixels_for_image[random_pixels_for_image < -0.5] = 0 - random_pixels_for_image[random_pixels_for_image != 0] = 1 - regions_without_separators[(random_pixels_for_image[:, :] == 1) & (text_regions_p[:, :] == 5)] = 1 + + #regions_without_separators_0 = regions_without_separators[:, :].sum(axis=0) + #meda_n_updown = regions_without_separators_0[len(regions_without_separators_0) :: -1] + #first_nonzero = next((i for i, x in enumerate(regions_without_separators_0) if x), 0) + #last_nonzero = next((i for i, x in enumerate(meda_n_updown) if x), 0) + #last_nonzero = len(regions_without_separators_0) - last_nonzero + + #random_pixels_for_image = np.random.randn(regions_without_separators.shape[0], regions_without_separators.shape[1]) + #random_pixels_for_image[random_pixels_for_image < -0.5] = 0 + #random_pixels_for_image[random_pixels_for_image != 0] = 1 + #regions_without_separators[(random_pixels_for_image[:, :] == 1) & (text_regions_p[:, :] == 5)] = 1 + + #regions_without_separators[:, 0:first_nonzero] = 0 + #regions_without_separators[:, last_nonzero:] = 0 else: regions_without_separators_d = regions_without_separators_d.astype(np.uint8) regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - random_pixels_for_image = np.random.randn(regions_without_separators_d.shape[0], regions_without_separators_d.shape[1]) - random_pixels_for_image[random_pixels_for_image < -0.5] = 0 - random_pixels_for_image[random_pixels_for_image != 0] = 1 - regions_without_separators_d[(random_pixels_for_image[:, :] == 1) & (text_regions_p_1_n[:, :] == 5)] = 1 + + #regions_without_separators_0 = regions_without_separators_d[:, :].sum(axis=0) + #meda_n_updown = regions_without_separators_0[len(regions_without_separators_0) :: -1] + #first_nonzero = next((i for i, x in enumerate(regions_without_separators_0) if x), 0) + #last_nonzero = next((i for i, x in enumerate(meda_n_updown) if x), 0) + #last_nonzero = len(regions_without_separators_0) - last_nonzero + + #random_pixels_for_image = np.random.randn(regions_without_separators_d.shape[0], regions_without_separators_d.shape[1]) + #random_pixels_for_image[random_pixels_for_image < -0.5] = 0 + #random_pixels_for_image[random_pixels_for_image != 0] = 1 + ##regions_without_separators_d[(random_pixels_for_image[:, :] == 1) & (text_regions_p_1_n[:, :] == 5)] = 1 + + #regions_without_separators_d[:, 0:first_nonzero] = 0 + #regions_without_separators_d[:, last_nonzero:] = 0 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 35c9201..2533455 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -360,7 +360,7 @@ def find_num_col_deskew(regions_without_separators, sigma_, multiplier=3.8): return np.std(z) -def find_num_col(regions_without_separators, multiplier=3.8): +def find_num_col(regions_without_separators, num_col_classifier, tables, multiplier=3.8): regions_without_separators_0 = regions_without_separators[:, :].sum(axis=0) ##plt.plot(regions_without_separators_0) ##plt.show() @@ -416,6 +416,19 @@ def find_num_col(regions_without_separators, multiplier=3.8): interest_neg_fin = interest_neg[(interest_neg < grenze)] peaks_neg_fin = peaks_neg[(interest_neg < grenze)] # interest_neg_fin=interest_neg[(interest_neg= 3: + index_sort_interest_neg_fin= np.argsort(interest_neg_fin) + peaks_neg_sorted = np.array(peaks_neg)[index_sort_interest_neg_fin] + interest_neg_fin_sorted = np.array(interest_neg_fin)[index_sort_interest_neg_fin] + + if len(index_sort_interest_neg_fin)>=num_col_classifier: + peaks_neg_fin = list( peaks_neg_sorted[:num_col_classifier] ) + interest_neg_fin = list( interest_neg_fin_sorted[:num_col_classifier] ) + else: + peaks_neg_fin = peaks_neg[:] + interest_neg_fin = interest_neg[:] num_col = (len(interest_neg_fin)) + 1 @@ -489,9 +502,9 @@ def find_num_col(regions_without_separators, multiplier=3.8): num_col = 1 peaks_neg_true = [] - diff_peaks_annormal = diff_peaks[diff_peaks < 360] + diff_peaks_abnormal = diff_peaks[diff_peaks < 360] - if len(diff_peaks_annormal) > 0: + if len(diff_peaks_abnormal) > 0: arg_help = np.array(range(len(diff_peaks))) arg_help_ann = arg_help[diff_peaks < 360] @@ -1248,7 +1261,7 @@ def return_points_with_boundies(peaks_neg_fin, first_point, last_point): peaks_neg_tot.append(last_point) return peaks_neg_tot -def find_number_of_columns_in_document(region_pre_p, num_col_classifier, pixel_lines, contours_h=None): +def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, pixel_lines, contours_h=None): separators_closeup=( (region_pre_p[:,:,:]==pixel_lines))*1 @@ -1561,7 +1574,7 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, pixel_l #regions_without_separators_tile=cv2.erode(regions_without_separators_tile,kernel,iterations = 3) # try: - num_col, peaks_neg_fin = find_num_col(regions_without_separators_tile,multiplier=7.0) + num_col, peaks_neg_fin = find_num_col(regions_without_separators_tile, num_col_classifier, tables, multiplier=7.0) except: num_col = 0 peaks_neg_fin = [] @@ -1583,7 +1596,7 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, pixel_l return num_col_fin, peaks_neg_fin_fin,matrix_of_lines_ch,splitter_y_new,separators_closeup_n -def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts): +def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, tables): boxes=[] peaks_neg_tot_tables = [] @@ -1599,20 +1612,21 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho try: if erosion_hurts: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],multiplier=6.) + num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], num_col_classifier, tables, multiplier=6.) else: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],multiplier=7.) + num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],num_col_classifier, tables, multiplier=7.) except: peaks_neg_fin=[] + num_col = 0 try: peaks_neg_fin_org=np.copy(peaks_neg_fin) - if (len(peaks_neg_fin)+1) Date: Tue, 13 Jul 2021 10:12:18 -0400 Subject: [PATCH 007/412] resolving an issue --- qurator/eynollah/eynollah.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ea4a60b..52d235b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2158,6 +2158,10 @@ class Eynollah: regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 + else: + text_regions_p_1_n = None + textline_mask_tot_d = None + regions_without_separators_d = None regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) regions_without_separators[table_prediction == 1] = 1 From 0859d22f4c808c9ba9df1183fd8ffb64f7480152 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 13 Jul 2021 19:58:08 -0400 Subject: [PATCH 008/412] modifications --- qurator/eynollah/eynollah.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 52d235b..664d835 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2275,19 +2275,20 @@ class Eynollah: text_regions_p[:, :][regions_fully_np[:, :, 0] == 4] = 4 #plt.imshow(text_regions_p) #plt.show() - if not self.tables: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout(image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) + ####if not self.tables: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout(image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) - text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) - textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) - regions_fully_n = resize_image(regions_fully_n, text_regions_p.shape[0], text_regions_p.shape[1]) + text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) + textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) + regions_fully_n = resize_image(regions_fully_n, text_regions_p.shape[0], text_regions_p.shape[1]) + if not self.tables: regions_without_separators_d = (text_regions_p_1_n[:, :] == 1) * 1 - else: - text_regions_p_1_n = None - textline_mask_tot_d = None - regions_without_separators_d = None - + else: + text_regions_p_1_n = None + textline_mask_tot_d = None + regions_without_separators_d = None + if not self.tables: regions_without_separators = (text_regions_p[:, :] == 1) * 1 K.clear_session() @@ -2342,7 +2343,6 @@ class Eynollah: if self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) - text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 @@ -2485,6 +2485,7 @@ class Eynollah: # print(len(contours_only_text_parent_h),len(contours_only_text_parent_h_d_ordered),'contours_only_text_parent_h') pixel_lines = 6 + if not self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: From 0e63ebcbe5bad3fcfbf5654aa3c4ce15f7a013da Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Mon, 16 Aug 2021 17:36:37 +0200 Subject: [PATCH 009/412] :package: v0.0.9 --- CHANGELOG.md | 13 ++++++++++++- qurator/eynollah/ocrd-tool.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a5193..d9f3be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased -## [0.0.7] - 2021-07-27 +## [0.0.9] - 2021-08-16 + +Added: + + * Table detection, #48 + +Fixed: + + * Catch exception, #47 + +## [0.0.8] - 2021-07-27 Fixed: @@ -50,6 +60,7 @@ Fixed: Initial release +[0.0.9]: ../../compare/v0.0.9...v0.0.8 [0.0.8]: ../../compare/v0.0.8...v0.0.7 [0.0.7]: ../../compare/v0.0.7...v0.0.6 [0.0.6]: ../../compare/v0.0.6...v0.0.5 diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index a51e77a..8b438ed 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.0.8", + "version": "0.0.9", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From c8f0feb5bd334a348a7ba29e24d28d93c3bb8960 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 21 Sep 2021 13:54:13 -0400 Subject: [PATCH 010/412] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c52560b..b05f386 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) -## Introduction +## Introductionss This tool performs document layout analysis (segmentation) from image data and returns the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). It can currently detect the following layout classes/elements: From 169b50aaaf8604d6e739686e4685bd1859729fd0 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 23 Sep 2021 15:51:30 -0400 Subject: [PATCH 011/412] fixed: empty page error due None table contours --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 664d835..be854b2 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2320,7 +2320,7 @@ class Eynollah: if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, []) + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) self.logger.info("Job done in %ss", str(time.time() - t1)) return pcgts From b018138cf8affd5eb2144ce606f85adabfa38ee0 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 23 Sep 2021 15:54:01 -0400 Subject: [PATCH 012/412] fixed: empty page error due None table contours --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b05f386..c52560b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) -## Introductionss +## Introduction This tool performs document layout analysis (segmentation) from image data and returns the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). It can currently detect the following layout classes/elements: From e769f625feefa9364d22be125ba40605c14596d1 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Mon, 27 Sep 2021 14:31:23 +0200 Subject: [PATCH 013/412] :package: v0.0.10 --- CHANGELOG.md | 7 +++++++ qurator/eynollah/ocrd-tool.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f3be8..f209001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.0.10] - 2021-09-27 + +Fixed: + + * call to `uild_pagexml_no_full_layout` for empty pages, #52 + ## [0.0.9] - 2021-08-16 Added: @@ -60,6 +66,7 @@ Fixed: Initial release +[0.0.10]: ../../compare/v0.0.10...v0.0.9 [0.0.9]: ../../compare/v0.0.9...v0.0.8 [0.0.8]: ../../compare/v0.0.8...v0.0.7 [0.0.7]: ../../compare/v0.0.7...v0.0.6 diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 8b438ed..d2d6d48 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.0.9", + "version": "0.0.10", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From d75803b11d5b543aab973126da1f0e2ff59a48dc Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Sun, 30 Jan 2022 16:08:44 +0100 Subject: [PATCH 014/412] ocrd-tool: "models" parameter is a directory --- qurator/eynollah/ocrd-tool.json | 1 + 1 file changed, 1 insertion(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index d2d6d48..4b20718 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -13,6 +13,7 @@ "models": { "type": "string", "format": "file", + "content-type": "text/directory", "cacheable": true, "description": "Path to directory containing models to be used (See https://qurator-data.de/eynollah)", "required": true From f0ac0bb0901b3f91c6ae626270af8fb4a8ec8855 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Wed, 2 Feb 2022 12:05:06 +0100 Subject: [PATCH 015/412] :package: v0.0.11 --- CHANGELOG.md | 7 +++++++ qurator/eynollah/ocrd-tool.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f209001..e8815d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.0.11] - 2022-02-02 + +Fixed: + + * `models` parameter should have `content-type`, #61, OCR-D/core#777 + ## [0.0.10] - 2021-09-27 Fixed: @@ -66,6 +72,7 @@ Fixed: Initial release +[0.0.11]: ../../compare/v0.0.11...v0.0.10 [0.0.10]: ../../compare/v0.0.10...v0.0.9 [0.0.9]: ../../compare/v0.0.9...v0.0.8 [0.0.8]: ../../compare/v0.0.8...v0.0.7 diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 4b20718..220f2ea 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.0.10", + "version": "0.0.11", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From cdea0acffec2b668db5479d8088cb1a5f0d1e34f Mon Sep 17 00:00:00 2001 From: "Gerber, Mike" Date: Tue, 8 Feb 2022 16:43:53 +0100 Subject: [PATCH 016/412] =?UTF-8?q?=F0=9F=92=84=20Improve=20timing=20messa?= =?UTF-8?q?ges=20(Fixes=20#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qurator/eynollah/eynollah.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index be854b2..c3f136b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2115,7 +2115,7 @@ class Eynollah: img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) - self.logger.info("detecting boxes took %ss", str(time.time() - t1)) + self.logger.info("detecting boxes took %.1fs", time.time() - t1) if self.tables: if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -2306,37 +2306,37 @@ class Eynollah: t0 = time.time() img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement() - self.logger.info("Enhancing took %ss ", str(time.time() - t0)) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) t1 = time.time() text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %ss ", str(time.time() - t1)) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) t1 = time.time() num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %ss ", str(time.time() - t1)) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) self.logger.info('cont_page %s', cont_page) if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) - self.logger.info("Job done in %ss", str(time.time() - t1)) + self.logger.info("Job done in %.1fs", time.time() - t1) return pcgts t1 = time.time() textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %ss", str(time.time() - t1)) + self.logger.info("textline detection took %.1fs", time.time() - t1) t1 = time.time() slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %ss", str(time.time() - t1)) + self.logger.info("deskewing took %.1fs", time.time() - t1) t1 = time.time() #plt.imshow(table_prediction) #plt.show() textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - self.logger.info("detection of marginals took %ss", str(time.time() - t1)) + self.logger.info("detection of marginals took %.1fs", time.time() - t1) t1 = time.time() if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) From 7ccd7663e17b6266ba169e7ab79f88ae7ddfdecd Mon Sep 17 00:00:00 2001 From: "Gerber, Mike" Date: Tue, 8 Feb 2022 17:45:24 +0100 Subject: [PATCH 017/412] =?UTF-8?q?=F0=9F=92=84=20Improve=20more=20timing?= =?UTF-8?q?=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qurator/eynollah/eynollah.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c3f136b..dbfee2b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2553,7 +2553,7 @@ class Eynollah: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) - self.logger.info("Job done in %ss", str(time.time() - t0)) + self.logger.info("Job done in %.1fs", time.time() - t0) return pcgts else: contours_only_text_parent_h = None @@ -2563,5 +2563,5 @@ class Eynollah: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) - self.logger.info("Job done in %ss", str(time.time() - t0)) + self.logger.info("Job done in %.1fs", time.time() - t0) return pcgts From 1fe8f92afc0783b63bcaef9c6138f3b3b771a887 Mon Sep 17 00:00:00 2001 From: "Gerber, Mike" Date: Tue, 8 Feb 2022 18:12:49 +0100 Subject: [PATCH 018/412] =?UTF-8?q?=F0=9F=90=9B=20Clarify=20message=20if?= =?UTF-8?q?=20an=20image=20was=20enhanced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qurator/eynollah/eynollah.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index be854b2..0f3eada 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -439,10 +439,10 @@ class Eynollah: image_res = self.predict_enhancement(img_new) is_image_enhanced = True else: - is_image_enhanced = False num_column_is_classified = True image_res = np.copy(img) - + is_image_enhanced = False + session_col_classifier.close() @@ -1986,9 +1986,9 @@ class Eynollah: return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction def run_enhancement(self): - self.logger.info("resize and enhance image") + self.logger.info("Resizing and enhancing image...") is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier() - self.logger.info("Image is %senhanced", '' if is_image_enhanced else 'not ') + self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') K.clear_session() scale = 1 if is_image_enhanced: From 11d9b00510c4b866ce934bfed9853c9df912e947 Mon Sep 17 00:00:00 2001 From: "Gerber, Mike" Date: Thu, 3 Mar 2022 12:21:40 +0100 Subject: [PATCH 019/412] =?UTF-8?q?=F0=9F=A7=B9=20Don't=20produce=20spurio?= =?UTF-8?q?us=20TextEquiv=20elements.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eynollah produces spurious - and empy - pcGts TextEquiv elements. This is a. unnecessary, b. wrong and c. produces a lot of warning messages in subsequent OCR processing steps because the OCR processor warns about already existing text. Fix this by not generating any TextEquiv elements. Fixes gh-37. --- qurator/eynollah/utils/xml.py | 1 - qurator/eynollah/writer.py | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/qurator/eynollah/utils/xml.py b/qurator/eynollah/utils/xml.py index ac02190..0386b25 100644 --- a/qurator/eynollah/utils/xml.py +++ b/qurator/eynollah/utils/xml.py @@ -21,7 +21,6 @@ from ocrd_models.ocrd_page import ( RegionRefType, SeparatorRegionType, TableRegionType, - TextEquivType, TextLineType, TextRegionType, UnorderedGroupIndexedType, diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index 2bacb17..d36d3ab 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -10,7 +10,6 @@ from ocrd_utils import getLogger from ocrd_models.ocrd_page import ( BorderType, CoordsType, - TextEquivType, PcGtsType, TextLineType, TextRegionType, @@ -59,7 +58,6 @@ class EynollahXmlWriter(): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) marginal_region.add_TextLine(textline) - textline.add_TextEquiv(TextEquivType(Unicode='')) points_co = '' for l in range(len(all_found_texline_polygons_marginals[marginal_idx][j])): if not self.curved_line: @@ -98,7 +96,7 @@ class EynollahXmlWriter(): self.logger.debug('enter serialize_lines_in_region') for j in range(len(all_found_texline_polygons[region_idx])): coords = CoordsType() - textline = TextLineType(id=counter.next_line_id, Coords=coords, TextEquiv=[TextEquivType(index=0, Unicode='')]) + textline = TextLineType(id=counter.next_line_id, Coords=coords) text_region.add_TextLine(textline) region_bboxes = all_box_coord[region_idx] points_co = '' @@ -158,7 +156,7 @@ class EynollahXmlWriter(): for mm in range(len(found_polygons_text_region)): textregion = TextRegionType(id=counter.next_region_id, type_='paragraph', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord)), - TextEquiv=[TextEquivType(index=0, Unicode='')]) + ) page.add_TextRegion(textregion) self.serialize_lines_in_region(textregion, all_found_texline_polygons, mm, page_coord, all_box_coord, slopes, counter) @@ -217,7 +215,6 @@ class EynollahXmlWriter(): for mm in range(len(found_polygons_text_region)): textregion = TextRegionType(id=counter.next_region_id, type_='paragraph', - TextEquiv=[TextEquivType(index=0, Unicode='')], Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord))) page.add_TextRegion(textregion) self.serialize_lines_in_region(textregion, all_found_texline_polygons, mm, page_coord, all_box_coord, slopes, counter) @@ -225,21 +222,18 @@ class EynollahXmlWriter(): self.logger.debug('len(found_polygons_text_region_h) %s', len(found_polygons_text_region_h)) for mm in range(len(found_polygons_text_region_h)): textregion = TextRegionType(id=counter.next_region_id, type_='header', - TextEquiv=[TextEquivType(index=0, Unicode='')], Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region_h[mm], page_coord))) page.add_TextRegion(textregion) self.serialize_lines_in_region(textregion, all_found_texline_polygons_h, mm, page_coord, all_box_coord_h, slopes_h, counter) for mm in range(len(found_polygons_marginals)): marginal = TextRegionType(id=counter.next_region_id, type_='marginalia', - TextEquiv=[TextEquivType(index=0, Unicode='')], Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_marginals[mm], page_coord))) page.add_TextRegion(marginal) self.serialize_lines_in_marginal(marginal, all_found_texline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) for mm in range(len(found_polygons_drop_capitals)): page.add_TextRegion(TextRegionType(id=counter.next_region_id, type_='drop-capital', - TextEquiv=[TextEquivType(index=0, Unicode='')], Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_drop_capitals[mm], page_coord)))) for mm in range(len(found_polygons_text_region_img)): From 2736ddb42d9c002f8c453b9e59aedf9fbe6fc9bc Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 10 Mar 2022 14:00:31 -0500 Subject: [PATCH 020/412] light version --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c52560b..7673954 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ Some heuristic methods are also employed to further improve the model prediction * After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. * Finally, using the derived coordinates, bounding boxes are determined for each textline. +## Light version +layout detection is implemented in lower scale and with only one model. + ## Installation `pip install .` or From b8a532180a836f08f0364dd0f350e70e73b77f0b Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 10 Mar 2022 23:52:10 -0500 Subject: [PATCH 021/412] light version integration --- qurator/eynollah/eynollah.py | 531 +++++++++++++++++++++++++++++++++-- 1 file changed, 511 insertions(+), 20 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 81c0b0c..478372b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -143,6 +143,7 @@ class Eynollah: self.model_region_dir_fully = dir_models + "/model_3up_new_good_no_augmentation.h5" self.model_page_dir = dir_models + "/model_page_mixed_best.h5" self.model_region_dir_p_ens = dir_models + "/model_ensemble_s.h5" + self.model_region_dir_p_ens_light = dir_models + "/model_11.h5" self.model_textline_dir = dir_models + "/model_textline_newspapers.h5" self.model_tables = dir_models + "/model_tables_ens_mixed_new_2.h5" @@ -378,10 +379,13 @@ class Eynollah: return img, img_new, is_image_enhanced - def resize_and_enhance_image_with_column_classifier(self): + def resize_and_enhance_image_with_column_classifier(self,light_version): self.logger.debug("enter resize_and_enhance_image_with_column_classifier") - dpi = self.dpi - self.logger.info("Detected %s DPI", dpi) + if light_version: + dpi = 300 + else: + dpi = self.dpi + self.logger.info("Detected %s DPI", dpi) if self.input_binary: img = self.imread() model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) @@ -637,6 +641,243 @@ class Eynollah: del model gc.collect() return prediction_true + def do_prediction_new_concept(self, patches, img, model, marginal_of_patch_percent=0.1): + self.logger.debug("enter do_prediction") + + img_height_model = model.layers[len(model.layers) - 1].output_shape[1] + img_width_model = model.layers[len(model.layers) - 1].output_shape[2] + + if not patches: + img_h_page = img.shape[0] + img_w_page = img.shape[1] + img = img / float(255.0) + img = resize_image(img, img_height_model, img_width_model) + + label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2])) + + seg = np.argmax(label_p_pred, axis=3)[0] + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + prediction_true = resize_image(seg_color, img_h_page, img_w_page) + prediction_true = prediction_true.astype(np.uint8) + + + else: + if img.shape[0] < img_height_model: + img = resize_image(img, img_height_model, img.shape[1]) + + if img.shape[1] < img_width_model: + img = resize_image(img, img.shape[0], img_width_model) + + self.logger.info("Image dimensions: %sx%s", img_height_model, img_width_model) + margin = int(marginal_of_patch_percent * img_height_model) + width_mid = img_width_model - 2 * margin + height_mid = img_height_model - 2 * margin + img = img / float(255.0) + img = img.astype(np.float16) + img_h = img.shape[0] + img_w = img.shape[1] + prediction_true = np.zeros((img_h, img_w, 3)) + mask_true = np.zeros((img_h, img_w)) + nxf = img_w / float(width_mid) + nyf = img_h / float(height_mid) + nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) + nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) + + for i in range(nxf): + for j in range(nyf): + if i == 0: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + else: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + if j == 0: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + else: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + if index_x_u > img_w: + index_x_u = img_w + index_x_d = img_w - img_width_model + if index_y_u > img_h: + index_y_u = img_h + index_y_d = img_h - img_height_model + + img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] + label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2])) + seg = np.argmax(label_p_pred, axis=3)[0] + + + seg_not_base = label_p_pred[0,:,:,4] + ##seg2 = -label_p_pred[0,:,:,2] + + + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + + + seg_test = label_p_pred[0,:,:,1] + ##seg2 = -label_p_pred[0,:,:,2] + + + seg_test[seg_test>0.75] =1 + seg_test[seg_test<1] =0 + + + seg_line = label_p_pred[0,:,:,3] + ##seg2 = -label_p_pred[0,:,:,2] + + + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + + seg_background = label_p_pred[0,:,:,0] + ##seg2 = -label_p_pred[0,:,:,2] + + + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + ##seg = seg+seg2 + #seg = label_p_pred[0,:,:,2] + #seg[seg>0.4] =1 + #seg[seg<1] =0 + + ##plt.imshow(seg_test) + ##plt.show() + + ##plt.imshow(seg_background) + ##plt.show() + #seg[seg==1]=0 + #seg[seg_test==1]=1 + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + + if i == 0 and j == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color + elif i == nxf - 1 and j == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] + mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg + prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg_color + elif i == 0 and j == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] + mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg + prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg_color + elif i == nxf - 1 and j == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] + mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color + elif i == 0 and j != 0 and j != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color + elif i == nxf - 1 and j != 0 and j != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] + mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color + elif i != 0 and i != nxf - 1 and j == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] + mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color + elif i != 0 and i != nxf - 1 and j == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] + mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg + prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] + mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color + + prediction_true = prediction_true.astype(np.uint8) + del model + gc.collect() + return prediction_true + + def early_page_for_num_of_column_classification(self,img_bin): + self.logger.debug("enter early_page_for_num_of_column_classification") + if self.input_binary: + img =np.copy(img_bin) + img = img.astype(np.uint8) + else: + img = self.imread() + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + img = cv2.GaussianBlur(img, (5, 5), 0) + + img_page_prediction = self.do_prediction(False, img, model_page) + + imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + thresh = cv2.dilate(thresh, KERNEL, iterations=3) + contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + if len(contours)>0: + cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt = contours[np.argmax(cnt_size)] + x, y, w, h = cv2.boundingRect(cnt) + box = [x, y, w, h] + else: + box = [0, 0, img.shape[1], img.shape[0]] + croped_page, page_coord = crop_image_inside_box(box, img) + session_page.close() + del model_page + del session_page + gc.collect() + K.clear_session() + self.logger.debug("exit early_page_for_num_of_column_classification") + return croped_page, page_coord + + def extract_page(self): + self.logger.debug("enter extract_page") + cont_page = [] + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + img = cv2.GaussianBlur(self.image, (5, 5), 0) + img_page_prediction = self.do_prediction(False, img, model_page) + imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + thresh = cv2.dilate(thresh, KERNEL, iterations=3) + contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + if len(contours)>0: + cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt = contours[np.argmax(cnt_size)] + x, y, w, h = cv2.boundingRect(cnt) + if x <= 30: + w += x + x = 0 + if (self.image.shape[1] - (x + w)) <= 30: + w = w + (self.image.shape[1] - (x + w)) + if y <= 30: + h = h + y + y = 0 + if (self.image.shape[0] - (y + h)) <= 30: + h = h + (self.image.shape[0] - (y + h)) + + box = [x, y, w, h] + else: + box = [0, 0, img.shape[1], img.shape[0]] + croped_page, page_coord = crop_image_inside_box(box, self.image) + cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) + session_page.close() + del model_page + del session_page + gc.collect() + K.clear_session() + self.logger.debug("exit extract_page") + return croped_page, page_coord, cont_page def early_page_for_num_of_column_classification(self,img_bin): self.logger.debug("enter early_page_for_num_of_column_classification") @@ -808,6 +1049,54 @@ class Eynollah: self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 + + def get_slopes_and_deskew_new_light(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): + self.logger.debug("enter get_slopes_and_deskew_new") + num_cores = cpu_count() + queue_of_all_params = Queue() + + processes = [] + nh = np.linspace(0, len(boxes), num_cores + 1) + indexes_by_text_con = np.array(range(len(contours_par))) + for i in range(num_cores): + boxes_per_process = boxes[int(nh[i]) : int(nh[i + 1])] + contours_per_process = contours[int(nh[i]) : int(nh[i + 1])] + contours_par_per_process = contours_par[int(nh[i]) : int(nh[i + 1])] + indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] + + processes.append(Process(target=self.do_work_of_slopes_new_light, args=(queue_of_all_params, boxes_per_process, textline_mask_tot, contours_per_process, contours_par_per_process, indexes_text_con_per_process, image_page_rotated, slope_deskew))) + for i in range(num_cores): + processes[i].start() + + slopes = [] + all_found_texline_polygons = [] + all_found_text_regions = [] + all_found_text_regions_par = [] + boxes = [] + all_box_coord = [] + all_index_text_con = [] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + slopes_for_sub_process = list_all_par[0] + polys_for_sub_process = list_all_par[1] + boxes_for_sub_process = list_all_par[2] + contours_for_subprocess = list_all_par[3] + contours_par_for_subprocess = list_all_par[4] + boxes_coord_for_subprocess = list_all_par[5] + indexes_for_subprocess = list_all_par[6] + for j in range(len(slopes_for_sub_process)): + slopes.append(slopes_for_sub_process[j]) + all_found_texline_polygons.append(polys_for_sub_process[j]) + boxes.append(boxes_for_sub_process[j]) + all_found_text_regions.append(contours_for_subprocess[j]) + all_found_text_regions_par.append(contours_par_for_subprocess[j]) + all_box_coord.append(boxes_coord_for_subprocess[j]) + all_index_text_con.append(indexes_for_subprocess[j]) + for i in range(num_cores): + processes[i].join() + self.logger.debug('slopes %s', slopes) + self.logger.debug("exit get_slopes_and_deskew_new") + return slopes, all_found_texline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con def get_slopes_and_deskew_new(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): self.logger.debug("enter get_slopes_and_deskew_new") @@ -1017,7 +1306,44 @@ class Eynollah: all_box_coord_per_process.append(crop_coor) queue_of_all_params.put([textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours, slopes_per_each_subprocess]) + def do_work_of_slopes_new_light(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, indexes_r_con_per_pro, image_page_rotated, slope_deskew): + self.logger.debug('enter do_work_of_slopes_new') + slopes_per_each_subprocess = [] + bounding_box_of_textregion_per_each_subprocess = [] + textlines_rectangles_per_each_subprocess = [] + contours_textregion_per_each_subprocess = [] + contours_textregion_par_per_each_subprocess = [] + all_box_coord_per_process = [] + index_by_text_region_contours = [] + for mv in range(len(boxes_text)): + _, crop_coor = crop_image_inside_box(boxes_text[mv],image_page_rotated) + mask_textline = np.zeros((textline_mask_tot_ea.shape)) + mask_textline = cv2.fillPoly(mask_textline,pts=[contours_per_process[mv]],color=(1,1,1)) + all_text_region_raw = (textline_mask_tot_ea*mask_textline[:,:])[boxes_text[mv][1]:boxes_text[mv][1]+boxes_text[mv][3] , boxes_text[mv][0]:boxes_text[mv][0]+boxes_text[mv][2] ] + all_text_region_raw=all_text_region_raw.astype(np.uint8) + slopes_per_each_subprocess.append([slope_deskew][0]) + mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) + mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) + + # plt.imshow(mask_only_con_region) + # plt.show() + all_text_region_raw = np.copy(textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]]) + mask_only_con_region = mask_only_con_region[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] + + + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, [slope_deskew][0], contours_par_per_process[mv], boxes_text[mv]) + + textlines_rectangles_per_each_subprocess.append(cnt_clean_rot) + index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) + bounding_box_of_textregion_per_each_subprocess.append(boxes_text[mv]) + + contours_textregion_per_each_subprocess.append(contours_per_process[mv]) + contours_textregion_par_per_each_subprocess.append(contours_par_per_process[mv]) + all_box_coord_per_process.append(crop_coor) + queue_of_all_params.put([slopes_per_each_subprocess, textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours]) + def do_work_of_slopes_new(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, indexes_r_con_per_pro, image_page_rotated, slope_deskew): self.logger.debug('enter do_work_of_slopes_new') slopes_per_each_subprocess = [] @@ -1144,6 +1470,110 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) + def get_regions_from_xy_2models_light(self,img,is_image_enhanced, num_col_classifier): + self.logger.debug("enter get_regions_from_xy_2models") + erosion_hurts = False + img_org = np.copy(img) + img_height_h = img_org.shape[0] + img_width_h = img_org.shape[1] + + #model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + + + + if num_col_classifier == 1: + img_w_new = 1000 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 1500 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + + elif num_col_classifier == 3: + img_w_new = 2000 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + + elif num_col_classifier == 4: + img_w_new = 2500 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + elif num_col_classifier == 5: + img_w_new = 3000 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + else: + img_w_new = 4000 + img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + gc.collect() + ##img_resized = resize_image(img_bin,img_height_h, img_width_h ) + img_resized = resize_image(img,img_h_new, img_w_new ) + + tbin = time.time() + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + print("time bin session", time.time()-tbin) + prediction_bin = self.do_prediction(True, img_resized, model_bin) + print("time bin all ", time.time()-tbin) + prediction_bin=prediction_bin[:,:,0] + prediction_bin = (prediction_bin[:,:]==0)*1 + prediction_bin = prediction_bin*255 + + prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) + + session_bin.close() + del model_bin + del session_bin + gc.collect() + + prediction_bin = prediction_bin.astype(np.uint16) + #img= np.copy(prediction_bin) + img_bin = np.copy(prediction_bin) + + + + + tline = time.time() + textline_mask_tot_ea = self.run_textline(img_bin) + print("time line all ", time.time()-tline) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + + + #plt.imshow(img_bin) + #plt.show() + + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + + #plt.imshow(prediction_regions_org[:,:,0]) + #plt.show() + + prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) + + prediction_regions_org=prediction_regions_org[:,:,0] + + mask_lines_only = (prediction_regions_org[:,:] ==3)*1 + + mask_texts_only = (prediction_regions_org[:,:] ==1)*1 + + mask_images_only=(prediction_regions_org[:,:] ==2)*1 + + polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) + polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + + + polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) + + polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) + + + text_regions_p_true = np.zeros(prediction_regions_org.shape) + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) + + text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) + + #erosion_hurts = True + K.clear_session() + return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_from_xy_2models") @@ -1939,7 +2369,54 @@ class Eynollah: return prediction_table_erode.astype(np.int16) + def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts): + img_g = self.imread(grayscale=True, uint8=True) + img_g3 = np.zeros((img_g.shape[0], img_g.shape[1], 3)) + img_g3 = img_g3.astype(np.uint8) + img_g3[:, :, 0] = img_g[:, :] + img_g3[:, :, 1] = img_g[:, :] + img_g3[:, :, 2] = img_g[:, :] + + image_page, page_coord, cont_page = self.extract_page() + + if self.tables: + table_prediction = self.get_tables_from_model(image_page, num_col_classifier) + else: + table_prediction = (np.zeros((image_page.shape[0], image_page.shape[1]))).astype(np.int16) + + if self.plotter: + self.plotter.save_page_image(image_page) + + text_regions_p_1 = text_regions_p_1[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + textline_mask_tot_ea = textline_mask_tot_ea[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + mask_images = (text_regions_p_1[:, :] == 2) * 1 + mask_images = mask_images.astype(np.uint8) + mask_images = cv2.erode(mask_images[:, :], KERNEL, iterations=10) + mask_lines = (text_regions_p_1[:, :] == 3) * 1 + mask_lines = mask_lines.astype(np.uint8) + img_only_regions_with_sep = ((text_regions_p_1[:, :] != 3) & (text_regions_p_1[:, :] != 0)) * 1 + img_only_regions_with_sep = img_only_regions_with_sep.astype(np.uint8) + + + if erosion_hurts: + img_only_regions = np.copy(img_only_regions_with_sep[:,:]) + else: + img_only_regions = cv2.erode(img_only_regions_with_sep[:,:], KERNEL, iterations=6) + + ##print(img_only_regions.shape,'img_only_regions') + ##plt.imshow(img_only_regions[:,:]) + ##plt.show() + num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) + try: + num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) + num_col = num_col + 1 + if not num_column_is_classified: + num_col_classifier = num_col + 1 + except Exception as why: + self.logger.error(why) + num_col = None + return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): img_g = self.imread(grayscale=True, uint8=True) @@ -1985,9 +2462,9 @@ class Eynollah: num_col = None return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction - def run_enhancement(self): + def run_enhancement(self,light_version): self.logger.info("Resizing and enhancing image...") - is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier() + is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier(light_version) self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') K.clear_session() scale = 1 @@ -2301,22 +2778,31 @@ class Eynollah: """ Get image and scales, then extract the page of scanned image """ + light_version = True self.logger.debug("enter run") t0 = time.time() - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement() + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) t1 = time.time() - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + if light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_from_xy_2models_light(img_res, is_image_enhanced, num_col_classifier) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - self.logger.info('cont_page %s', cont_page) + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + self.logger.info('cont_page %s', cont_page) if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") @@ -2325,12 +2811,13 @@ class Eynollah: return pcgts t1 = time.time() - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) + if not light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) - t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) t1 = time.time() #plt.imshow(table_prediction) #plt.show() @@ -2455,8 +2942,12 @@ class Eynollah: boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) if not self.curved_line: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + if light_version: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: scale_param = 1 From cf5ef8f5ae8bdf194dab6c4b9ba06824f25a41d5 Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 14 Mar 2022 11:37:32 -0400 Subject: [PATCH 022/412] light version as option --- qurator/eynollah/cli.py | 10 +++++++++- qurator/eynollah/eynollah.py | 11 ++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index f343918..6aabbae 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -92,11 +92,17 @@ from qurator.eynollah.eynollah import Eynollah help="if this parameter set to true, this tool would check the scale and if needed it will scale it to perform better layout detection", ) @click.option( - "--headers-off/--headers-on", + "--headers_off/--headers-on", "-ho/-noho", is_flag=True, help="if this parameter set to true, this tool would ignore headers role in reading order", ) +@click.option( + "--light_version/--original", + "-light/-org", + is_flag=True, + help="if this parameter set to true, this tool would use lighter version", +) @click.option( "--log-level", "-l", @@ -119,6 +125,7 @@ def main( input_binary, allow_scaling, headers_off, + light_version, log_level ): if log_level: @@ -146,6 +153,7 @@ def main( input_binary=input_binary, allow_scaling=allow_scaling, headers_off=headers_off, + light_version=light_version, ) pcgts = eynollah.run() eynollah.writer.write_pagexml(pcgts) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 478372b..62ae6de 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -100,6 +100,7 @@ class Eynollah: input_binary=False, allow_scaling=False, headers_off=False, + light_version=False, override_dpi=None, logger=None, pcgts=None, @@ -119,6 +120,7 @@ class Eynollah: self.input_binary = input_binary self.allow_scaling = allow_scaling self.headers_off = headers_off + self.light_version = light_version self.plotter = None if not enable_plotting else EynollahPlotter( dir_out=self.dir_out, dir_of_all=dir_of_all, @@ -2778,16 +2780,15 @@ class Eynollah: """ Get image and scales, then extract the page of scanned image """ - light_version = True self.logger.debug("enter run") t0 = time.time() - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(light_version) + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) t1 = time.time() - if light_version: + if self.light_version: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_from_xy_2models_light(img_res, is_image_enhanced, num_col_classifier) slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) @@ -2811,7 +2812,7 @@ class Eynollah: return pcgts t1 = time.time() - if not light_version: + if not self.light_version: textline_mask_tot_ea = self.run_textline(image_page) self.logger.info("textline detection took %.1fs", time.time() - t1) @@ -2942,7 +2943,7 @@ class Eynollah: boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) if not self.curved_line: - if light_version: + if self.light_version: slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: From 571dc84c3f0ef66f41c63e9ad722fae6a1668e5e Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Mon, 28 Mar 2022 13:15:35 +0200 Subject: [PATCH 023/412] README.md cleanup / restructuring --- README.md | 95 ++++++------------------------------------------------- 1 file changed, 9 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index c52560b..a9a86e8 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,8 @@ # Eynollah -> Document Layout Analysis +> Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) -## Introduction -This tool performs document layout analysis (segmentation) from image data and returns the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). - -It can currently detect the following layout classes/elements: -* [Border](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_BorderType.html) -* [Textregion](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html) -* [Textline](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html) -* [Image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html) -* [Separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html) -* [Marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html) -* [Initial (Drop Capital)](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html) - -In addition, the tool can be used to detect the _[ReadingOrder](https://ocr-d.de/en/gt-guidelines/trans/lyLeserichtung.html)_ of regions. The final goal is to feed the output to an OCR model. - -The tool uses a combination of various models and heuristics (see flowchart below for the different stages and how they interact): -* [Border detection](https://github.com/qurator-spk/eynollah#border-detection) -* [Layout detection](https://github.com/qurator-spk/eynollah#layout-detection) -* [Textline detection](https://github.com/qurator-spk/eynollah#textline-detection) -* [Image enhancement](https://github.com/qurator-spk/eynollah#Image_enhancement) -* [Scale classification](https://github.com/qurator-spk/eynollah#Scale_classification) -* [Heuristic methods](https://https://github.com/qurator-spk/eynollah#heuristic-methods) - -The first three stages are based on [pixel-wise segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). - -![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) - -## Border detection -For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. - -## Layout detection -As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. - -## Textline detection -In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. - -## Image enhancement -This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. - -## Scale classification -This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. - -## Heuristic methods -Some heuristic methods are also employed to further improve the model predictions: -* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. -* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. -* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. -* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. -* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. -* Finally, using the derived coordinates, bounding boxes are determined for each textline. - ## Installation `pip install .` or @@ -66,13 +16,17 @@ Alternatively, you can also use `make` with these targets: ### Models -In order to run this tool you also need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). +In order to run this tool you need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). Alternatively, running `make models` will download and extract models to `$(PWD)/models_eynollah`. +### Training + +In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + ## Usage -The basic command-line interface can be called like this: +The command-line interface can be called like this: ```sh eynollah \ @@ -94,37 +48,6 @@ eynollah \ ``` -The tool does accept and works better on original images (RGB format) than binarized images. +The tool performs better with RGB images than greyscale/binarized images. -### `--full-layout` vs `--no-full-layout` - -Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: - -| | `--full-layout` | `--no-full-layout` | -| --- | --- | --- | -| reading order | x | x | -| header regions | x | - | -| text regions | x | x | -| text regions / text line | x | x | -| drop-capitals | x | - | -| marginals | x | x | -| marginals / text line | x | x | -| image region | x | x | - -### How to use - -First, this model makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. - -* If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. - -* If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi (pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. - -* For some documents, while the quality is good, their scale is very large, and the performance of tool decreases. In such cases you can set `-as` (**a**llow **s**caling) to `true`. With this option enabled, the tool will try to rescale the image and only then the layout detection process will begin. - -* If you care about drop capitals (initials) and headings, you can set `-fl` (**f**ull **l**ayout) to `true`. With this setting, the tool can currently distinguish 7 document layout classes/elements. - -* In cases where the document includes curved headers or curved lines, rectangular bounding boxes for textlines will not be a great option. In such cases it is strongly recommended setting the flag `-cl` (**c**urved **l**ines) to `true` to find contours of curved lines instead of rectangular bounding boxes. Be advised that enabling this option increases the processing time of the tool. - -* To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide a directory path to store the extracted images. - -* This tool is actively being developed. If problems occur, or the performance does not meet your expectations, we welcome your feedback via [issues](https://github.com/qurator-spk/eynollah/issues). +Additional documentation can be found in the [wiki](https://github.com/qurator-spk/eynollah/wiki). From c606391c312eceab9aa3ebff071bdf12a30b45cc Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 29 Mar 2022 06:55:19 -0400 Subject: [PATCH 024/412] flow from directory --- qurator/eynollah/cli.py | 14 +- qurator/eynollah/eynollah.py | 977 ++++++++++++++++------------- qurator/eynollah/utils/__init__.py | 80 ++- qurator/eynollah/utils/contour.py | 133 +++- 4 files changed, 767 insertions(+), 437 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 6aabbae..ca938c4 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -10,7 +10,6 @@ from qurator.eynollah.eynollah import Eynollah "-i", help="image filename", type=click.Path(exists=True, dir_okay=False), - required=True, ) @click.option( "--out", @@ -19,6 +18,12 @@ from qurator.eynollah.eynollah import Eynollah type=click.Path(exists=True, file_okay=False), required=True, ) +@click.option( + "--dir_in", + "-di", + help="directory of images", + type=click.Path(exists=True, file_okay=False), +) @click.option( "--model", "-m", @@ -112,6 +117,7 @@ from qurator.eynollah.eynollah import Eynollah def main( image, out, + dir_in, model, save_images, save_layout, @@ -140,6 +146,7 @@ def main( eynollah = Eynollah( image_filename=image, dir_out=out, + dir_in=dir_in, dir_models=model, dir_of_cropped_images=save_images, dir_of_layout=save_layout, @@ -155,8 +162,9 @@ def main( headers_off=headers_off, light_version=light_version, ) - pcgts = eynollah.run() - eynollah.writer.write_pagexml(pcgts) + eynollah.run() + #pcgts = eynollah.run() + ##eynollah.writer.write_pagexml(pcgts) if __name__ == "__main__": main() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 62ae6de..b3fca7b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -29,6 +29,7 @@ warnings.filterwarnings("ignore") from scipy.signal import find_peaks import matplotlib.pyplot as plt from scipy.ndimage import gaussian_filter1d +from keras.backend import set_session from .utils.contour import ( filter_contours_area_of_image, @@ -38,6 +39,7 @@ from .utils.contour import ( find_features_of_contours, get_text_region_boxes_by_given_contours, get_textregion_contours_in_org_image, + get_textregion_contours_in_org_image_light, return_contours_of_image, return_contours_of_interested_region, return_contours_of_interested_region_by_min_size, @@ -65,6 +67,7 @@ from .utils import ( put_drop_out_from_only_drop_model, putt_bb_of_drop_capitals_of_model_in_patches_in_layout, check_any_text_region_in_model_one_is_main_or_header, + check_any_text_region_in_model_one_is_main_or_header_light, small_textlines_to_parent_adherence2, order_of_regions, find_number_of_columns_in_document, @@ -84,10 +87,11 @@ class Eynollah: def __init__( self, dir_models, - image_filename, + image_filename=None, image_pil=None, image_filename_stem=None, dir_out=None, + dir_in=None, dir_of_cropped_images=None, dir_of_layout=None, dir_of_deskewed=None, @@ -105,14 +109,16 @@ class Eynollah: logger=None, pcgts=None, ): - if image_pil: - self._imgs = self._cache_images(image_pil=image_pil) - else: - self._imgs = self._cache_images(image_filename=image_filename) - if override_dpi: - self.dpi = override_dpi - self.image_filename = image_filename + if not dir_in: + if image_pil: + self._imgs = self._cache_images(image_pil=image_pil) + else: + self._imgs = self._cache_images(image_filename=image_filename) + if override_dpi: + self.dpi = override_dpi + self.image_filename = image_filename self.dir_out = dir_out + self.dir_in = dir_in self.allow_enhancement = allow_enhancement self.curved_line = curved_line self.full_layout = full_layout @@ -121,6 +127,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.light_version = light_version + self.pcgts = pcgts self.plotter = None if not enable_plotting else EynollahPlotter( dir_out=self.dir_out, dir_of_all=dir_of_all, @@ -128,11 +135,12 @@ class Eynollah: dir_of_cropped_images=dir_of_cropped_images, dir_of_layout=dir_of_layout, image_filename_stem=Path(Path(image_filename).name).stem) - self.writer = EynollahXmlWriter( - dir_out=self.dir_out, - image_filename=self.image_filename, - curved_line=self.curved_line, - pcgts=pcgts) + if not dir_in: + self.writer = EynollahXmlWriter( + dir_out=self.dir_out, + image_filename=self.image_filename, + curved_line=self.curved_line, + pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') self.dir_models = dir_models @@ -149,6 +157,41 @@ class Eynollah: self.model_textline_dir = dir_models + "/model_textline_newspapers.h5" self.model_tables = dir_models + "/model_tables_ens_mixed_new_2.h5" + if dir_in and light_version: + config = tf.compat.v1.ConfigProto() + config.gpu_options.allow_growth = True + session = tf.compat.v1.Session(config=config) + set_session(session) + + self.model_page = self.our_load_model(self.model_page_dir) + self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) + self.model_bin = self.our_load_model(self.model_dir_of_binarization) + self.model_textline = self.our_load_model(self.model_textline_dir) + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) + self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) + self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + + self.ls_imgs = os.listdir(self.dir_in) + + if dir_in and not light_version: + config = tf.compat.v1.ConfigProto() + config.gpu_options.allow_growth = True + session = tf.compat.v1.Session(config=config) + set_session(session) + + self.model_page = self.our_load_model(self.model_page_dir) + self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) + self.model_bin = self.our_load_model(self.model_dir_of_binarization) + self.model_textline = self.our_load_model(self.model_textline_dir) + self.model_region = self.our_load_model(self.model_region_dir_p_ens) + self.model_region_p2 = self.our_load_model(self.model_region_dir_p2) + self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) + self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) + + self.ls_imgs = os.listdir(self.dir_in) + + def _cache_images(self, image_filename=None, image_pil=None): ret = {} if image_filename: @@ -161,7 +204,15 @@ class Eynollah: for prefix in ('', '_grayscale'): ret[f'img{prefix}_uint8'] = ret[f'img{prefix}'].astype(np.uint8) return ret - + def reset_file_name_dir(self, image_filename): + self._imgs = self._cache_images(image_filename=image_filename) + self.image_filename = image_filename + + self.writer = EynollahXmlWriter( + dir_out=self.dir_out, + image_filename=self.image_filename, + curved_line=self.curved_line, + pcgts=self.pcgts) def imread(self, grayscale=False, uint8=True): key = 'img' if grayscale: @@ -335,7 +386,8 @@ class Eynollah: img = self.imread() _, page_coord = self.early_page_for_num_of_column_classification(img) - model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) + if not self.dir_in: + model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) if self.input_binary: img_in = np.copy(img) img_in = img_in / 255.0 @@ -357,18 +409,19 @@ class Eynollah: img_in[0, :, :, 0] = img_1ch[:, :] img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] - - label_p_pred = model_num_classifier.predict(img_in) + if not self.dir_in: + label_p_pred = model_num_classifier.predict(img_in) + else: + label_p_pred = self.model_classifier.predict(img_in) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - - session_col_classifier.close() - - del model_num_classifier - del session_col_classifier - - K.clear_session() + if not self.dir_in: + session_col_classifier.close() + + del model_num_classifier + del session_col_classifier + K.clear_session() gc.collect() @@ -383,25 +436,27 @@ class Eynollah: def resize_and_enhance_image_with_column_classifier(self,light_version): self.logger.debug("enter resize_and_enhance_image_with_column_classifier") - if light_version: - dpi = 300 - else: - dpi = self.dpi - self.logger.info("Detected %s DPI", dpi) + dpi = self.dpi + self.logger.info("Detected %s DPI", dpi) if self.input_binary: img = self.imread() - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img, model_bin) + if self.dir_in: + prediction_bin = self.do_prediction(True, img, self.model_bin) + else: + + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img, model_bin) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - session_bin.close() - del model_bin - del session_bin + + if not self.dir_in: + session_bin.close() + del model_bin + del session_bin gc.collect() prediction_bin = prediction_bin.astype(np.uint8) @@ -412,7 +467,8 @@ class Eynollah: img_bin = None _, page_coord = self.early_page_for_num_of_column_classification(img_bin) - model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) + if not self.dir_in: + model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) if self.input_binary: img_in = np.copy(img) @@ -433,23 +489,29 @@ class Eynollah: img_in[0, :, :, 2] = img_1ch[:, :] - - label_p_pred = model_num_classifier.predict(img_in) + if self.dir_in: + label_p_pred = self.model_classifier.predict(img_in) + else: + label_p_pred = model_num_classifier.predict(img_in) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - session_col_classifier.close() - K.clear_session() + if not self.dir_in: + session_col_classifier.close() + K.clear_session() if dpi < DPI_THRESHOLD: img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) - image_res = self.predict_enhancement(img_new) + if light_version: + image_res = np.copy(img_new) + else: + image_res = self.predict_enhancement(img_new) is_image_enhanced = True else: num_column_is_classified = True image_res = np.copy(img) is_image_enhanced = False - - session_col_classifier.close() + if not self.dir_in: + session_col_classifier.close() self.logger.debug("exit resize_and_enhance_image_with_column_classifier") @@ -595,48 +657,48 @@ class Eynollah: if i == 0 and j == 0: seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + #seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color elif i == nxf - 1 and j == nyf - 1: seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg + #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg_color elif i == 0 and j == nyf - 1: seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg + #seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg_color elif i == nxf - 1 and j == 0: seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color elif i == 0 and j != 0 and j != nyf - 1: seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + #seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color elif i == nxf - 1 and j != 0 and j != nyf - 1: seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color elif i != 0 and i != nxf - 1 and j == 0: seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color elif i != 0 and i != nxf - 1 and j == nyf - 1: seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg + #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg_color else: seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color prediction_true = prediction_true.astype(np.uint8) @@ -817,10 +879,13 @@ class Eynollah: img = img.astype(np.uint8) else: img = self.imread() - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(img, (5, 5), 0) - - img_page_prediction = self.do_prediction(False, img, model_page) + if self.dir_in: + img_page_prediction = self.do_prediction(False, img, self.model_page) + else: + img_page_prediction = self.do_prediction(False, img, model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -834,20 +899,25 @@ class Eynollah: else: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, img) - session_page.close() - del model_page - del session_page + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() gc.collect() - K.clear_session() self.logger.debug("exit early_page_for_num_of_column_classification") return croped_page, page_coord def extract_page(self): self.logger.debug("enter extract_page") cont_page = [] - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(self.image, (5, 5), 0) - img_page_prediction = self.do_prediction(False, img, model_page) + if not self.dir_in: + img_page_prediction = self.do_prediction(False, img, model_page) + else: + img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) thresh = cv2.dilate(thresh, KERNEL, iterations=3) @@ -873,11 +943,12 @@ class Eynollah: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, self.image) cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - session_page.close() - del model_page - del session_page + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() gc.collect() - K.clear_session() self.logger.debug("exit extract_page") return croped_page, page_coord, cont_page @@ -888,10 +959,14 @@ class Eynollah: img = img.astype(np.uint8) else: img = self.imread() - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(img, (5, 5), 0) - - img_page_prediction = self.do_prediction(False, img, model_page) + + if self.dir_in: + img_page_prediction = self.do_prediction(False, img, self.model_page) + else: + img_page_prediction = self.do_prediction(False, img, model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -905,20 +980,28 @@ class Eynollah: else: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, img) - session_page.close() - del model_page - del session_page + + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() + gc.collect() - K.clear_session() + self.logger.debug("exit early_page_for_num_of_column_classification") return croped_page, page_coord def extract_page(self): self.logger.debug("enter extract_page") cont_page = [] - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(self.image, (5, 5), 0) - img_page_prediction = self.do_prediction(False, img, model_page) + if not self.dir_in: + img_page_prediction = self.do_prediction(False, img, model_page) + else: + img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) thresh = cv2.dilate(thresh, KERNEL, iterations=3) @@ -944,11 +1027,12 @@ class Eynollah: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, self.image) cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - session_page.close() - del model_page - del session_page + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() gc.collect() - K.clear_session() self.logger.debug("exit extract_page") return croped_page, page_coord, cont_page @@ -956,8 +1040,10 @@ class Eynollah: self.logger.debug("enter extract_text_regions") img_height_h = img.shape[0] img_width_h = img.shape[1] - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully if patches else self.model_region_dir_fully_np) + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully if patches else self.model_region_dir_fully_np) + else: + model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: img = otsu_copy_binary(img) @@ -1043,10 +1129,11 @@ class Eynollah: marginal_of_patch_percent = 0.1 prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) - - session_region.close() - del model_region - del session_region + + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() self.logger.debug("exit extract_text_regions") @@ -1422,19 +1509,26 @@ class Eynollah: def textline_contours(self, img, patches, scaler_h, scaler_w): self.logger.debug('enter textline_contours') - - model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) + if not self.dir_in: + model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) img = img.astype(np.uint8) img_org = np.copy(img) img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - prediction_textline = self.do_prediction(patches, img, model_textline) + if not self.dir_in: + prediction_textline = self.do_prediction(patches, img, model_textline) + else: + prediction_textline = self.do_prediction(patches, img, self.model_textline) prediction_textline = resize_image(prediction_textline, img_h, img_w) - prediction_textline_longshot = self.do_prediction(False, img, model_textline) + if not self.dir_in: + prediction_textline_longshot = self.do_prediction(False, img, model_textline) + else: + prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) - - session_textline.close() + + if not self.dir_in: + session_textline.close() return prediction_textline[:, :, 0], prediction_textline_longshot_true_size[:, :, 0] @@ -1508,20 +1602,20 @@ class Eynollah: ##img_resized = resize_image(img_bin,img_height_h, img_width_h ) img_resized = resize_image(img,img_h_new, img_w_new ) - tbin = time.time() - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - print("time bin session", time.time()-tbin) - prediction_bin = self.do_prediction(True, img_resized, model_bin) - print("time bin all ", time.time()-tbin) + if not self.dir_in: + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_resized, model_bin) + else: + prediction_bin = self.do_prediction(True, img_resized, self.model_bin) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - session_bin.close() - del model_bin - del session_bin + if not self.dir_in: + session_bin.close() + del model_bin + del session_bin gc.collect() prediction_bin = prediction_bin.astype(np.uint16) @@ -1530,18 +1624,14 @@ class Eynollah: - - tline = time.time() textline_mask_tot_ea = self.run_textline(img_bin) - print("time line all ", time.time()-tline) - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - - - #plt.imshow(img_bin) - #plt.show() - - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) - + + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + else: + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) + #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() @@ -1564,7 +1654,6 @@ class Eynollah: polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) - text_regions_p_true = np.zeros(prediction_regions_org.shape) text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) @@ -1574,7 +1663,8 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) #erosion_hurts = True - K.clear_session() + if not self.dir_in: + K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): @@ -1583,15 +1673,18 @@ class Eynollah: img_org = np.copy(img) img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1.3 ratio_x=1 img = resize_image(img_org, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - - prediction_regions_org_y = self.do_prediction(True, img, model_region) + if not self.dir_in: + prediction_regions_org_y = self.do_prediction(True, img, model_region) + else: + prediction_regions_org_y = self.do_prediction(True, img, self.model_region) prediction_regions_org_y = resize_image(prediction_regions_org_y, img_height_h, img_width_h ) #plt.imshow(prediction_regions_org_y[:,:,0]) @@ -1609,8 +1702,11 @@ class Eynollah: _, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1]*(1.2 if is_image_enhanced else 1))) - - prediction_regions_org = self.do_prediction(True, img, model_region) + + if self.dir_in: + prediction_regions_org = self.do_prediction(True, img, self.model_region) + else: + prediction_regions_org = self.do_prediction(True, img, model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) ##plt.imshow(prediction_regions_org[:,:,0]) @@ -1618,20 +1714,26 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] prediction_regions_org[(prediction_regions_org[:,:]==1) & (mask_zeros_y[:,:]==1)]=0 - session_region.close() - del model_region - del session_region + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p2) + + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p2) img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1])) - prediction_regions_org2 = self.do_prediction(True, img, model_region, 0.2) + + if self.dir_in: + prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, 0.2) + else: + prediction_regions_org2 = self.do_prediction(True, img, model_region, 0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) - - session_region.close() - del model_region - del session_region + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() mask_zeros2 = (prediction_regions_org2[:,:,0] == 0) @@ -1663,8 +1765,11 @@ class Eynollah: if self.input_binary: prediction_bin = np.copy(img_org) else: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin) + if not self.dir_in: + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_org, model_bin) + else: + prediction_bin = self.do_prediction(True, img_org, self.model_bin) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] @@ -1672,29 +1777,34 @@ class Eynollah: prediction_bin = prediction_bin*255 prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - session_bin.close() - del model_bin - del session_bin + + if not self.dir_in: + session_bin.close() + del model_bin + del session_bin gc.collect() - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 ratio_x=1 img = resize_image(prediction_bin, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - - prediction_regions_org = self.do_prediction(True, img, model_region) + + if not self.dir_in: + prediction_regions_org = self.do_prediction(True, img, model_region) + else: + prediction_regions_org = self.do_prediction(True, img, self.model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] mask_lines_only=(prediction_regions_org[:,:]==3)*1 - session_region.close() - del model_region - del session_region + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() @@ -1716,21 +1826,25 @@ class Eynollah: text_regions_p_true=cv2.fillPoly(text_regions_p_true,pts=polygons_of_only_texts, color=(1,1,1)) - - K.clear_session() + if not self.dir_in: + K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml except: if self.input_binary: prediction_bin = np.copy(img_org) else: - session_region.close() - del model_region - del session_region + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin) + if not self.dir_in: + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_org, model_bin) + else: + prediction_bin = self.do_prediction(True, img_org, self.model_bin) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] @@ -1741,29 +1855,32 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - session_bin.close() - del model_bin - del session_bin + if not self.dir_in: + session_bin.close() + del model_bin + del session_bin gc.collect() - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 ratio_x=1 img = resize_image(prediction_bin, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - - prediction_regions_org = self.do_prediction(True, img, model_region) + if not self.dir_in: + prediction_regions_org = self.do_prediction(True, img, model_region) + else: + prediction_regions_org = self.do_prediction(True, img, self.model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] #mask_lines_only=(prediction_regions_org[:,:]==3)*1 - session_region.close() - del model_region - del session_region + if not self.dir_in: + session_region.close() + del model_region + del session_region gc.collect() #img = resize_image(img_org, int(img_org.shape[0]*1), int(img_org.shape[1]*1)) @@ -1807,7 +1924,8 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) erosion_hurts = True - K.clear_session() + if not self.dir_in: + K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml def do_order_of_regions_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -2409,7 +2527,7 @@ class Eynollah: ##print(img_only_regions.shape,'img_only_regions') ##plt.imshow(img_only_regions[:,:]) ##plt.show() - num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) + ##num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) try: num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) num_col = num_col + 1 @@ -2468,7 +2586,8 @@ class Eynollah: self.logger.info("Resizing and enhancing image...") is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier(light_version) self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') - K.clear_session() + if not self.dir_in: + K.clear_session() scale = 1 if is_image_enhanced: if self.allow_enhancement: @@ -2492,7 +2611,8 @@ class Eynollah: scaler_h_textline = 1 # 1.2#1.2 scaler_w_textline = 1 # 0.9#1 textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline) - K.clear_session() + if not self.dir_in: + K.clear_session() if self.plotter: self.plotter.save_plot_of_textlines(textline_mask_tot_ea, image_page) return textline_mask_tot_ea @@ -2554,7 +2674,8 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - K.clear_session() + if not self.dir_in: + K.clear_session() self.logger.info("num_col_classifier: %s", num_col_classifier) @@ -2619,8 +2740,8 @@ class Eynollah: pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) - - K.clear_session() + if not self.dir_in: + K.clear_session() self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables @@ -2651,7 +2772,8 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) - K.clear_session() + if not self.dir_in: + K.clear_session() gc.collect() if num_col_classifier>=3: @@ -2718,22 +2840,24 @@ class Eynollah: text_regions_p[:, :][text_regions_p[:, :] == 2] = 5 text_regions_p[:, :][text_regions_p[:, :] == 3] = 6 text_regions_p[:, :][text_regions_p[:, :] == 4] = 8 - - K.clear_session() + if not self.dir_in: + K.clear_session() image_page = image_page.astype(np.uint8) regions_fully, regions_fully_only_drop = self.extract_text_regions(image_page, True, cols=num_col_classifier) text_regions_p[:,:][regions_fully[:,:,0]==6]=6 regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 - K.clear_session() + if not self.dir_in: + K.clear_session() # plt.imshow(regions_fully[:,:,0]) # plt.show() regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully) # plt.imshow(regions_fully[:,:,0]) # plt.show() - K.clear_session() + if not self.dir_in: + K.clear_session() regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) # plt.imshow(regions_fully_np[:,:,0]) # plt.show() @@ -2744,7 +2868,8 @@ class Eynollah: # plt.imshow(regions_fully_np[:,:,0]) # plt.show() - K.clear_session() + if not self.dir_in: + K.clear_session() # plt.imshow(regions_fully[:,:,0]) # plt.show() regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) @@ -2769,12 +2894,18 @@ class Eynollah: regions_without_separators_d = None if not self.tables: regions_without_separators = (text_regions_p[:, :] == 1) * 1 - - K.clear_session() + if not self.dir_in: + K.clear_session() img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) self.logger.debug('exit run_boxes_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables + + def our_load_model(self, model_file): + + model = load_model(model_file, compile=False) + + return model def run(self): """ @@ -2782,278 +2913,270 @@ class Eynollah: """ self.logger.debug("enter run") - t0 = time.time() - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - - self.logger.info("Enhancing took %.1fs ", time.time() - t0) + t0_tot = time.time() - t1 = time.time() - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_from_xy_2models_light(img_res, is_image_enhanced, num_col_classifier) + if not self.dir_in: + self.ls_imgs = [1] + + for img_name in self.ls_imgs: + t0 = time.time() + if self.dir_in: + self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) + + t1 = time.time() + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_from_xy_2models_light(img_res, is_image_enhanced, num_col_classifier) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) + + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) + self.logger.info("Job done in %.1fs", time.time() - t1) + return pcgts t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - self.logger.info('cont_page %s', cont_page) - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) - self.logger.info("Job done in %.1fs", time.time() - t1) - return pcgts - - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() + #plt.imshow(table_prediction) + #plt.show() - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - - if self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] - - index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - - areas_cnt_text_d = np.array([cv2.contourArea(contours_only_text_parent_d[j]) for j in range(len(contours_only_text_parent_d))]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d=np.argsort(areas_cnt_text_d) - contours_only_text_parent_d=list(np.array(contours_only_text_parent_d)[index_con_parents_d] ) - areas_cnt_text_d=list(np.array(areas_cnt_text_d)[index_con_parents_d] ) - - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + if self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + + min_con_area = 0.000005 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big + index_con_parents = np.argsort(areas_cnt_text_parent) + contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(contours_only_text_parent_d[j]) for j in range(len(contours_only_text_parent_d))]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d=np.argsort(areas_cnt_text_d) + contours_only_text_parent_d=list(np.array(contours_only_text_parent_d)[index_con_parents_d] ) + areas_cnt_text_d=list(np.array(areas_cnt_text_d)[index_con_parents_d] ) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] - index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + index_con_parents = np.argsort(areas_cnt_text_parent) + contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - - if not self.curved_line: + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) + # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) + else: + pass if self.light_version: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) else: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - scale_param = 1 - all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_texline_polygons = small_textlines_to_parent_adherence2(all_found_texline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_texline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_texline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - K.clear_session() - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + if not self.curved_line: + if self.light_version: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: - contours_only_text_parent_d_ordered = None - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + + scale_param = 1 + all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_texline_polygons = small_textlines_to_parent_adherence2(all_found_texline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_texline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_texline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + if not self.dir_in: + K.clear_session() + + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + if self.plotter: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) + if not self.dir_in: + K.clear_session() + + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_texline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) + pixel_lines = 6 + + + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + + + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - K.clear_session() - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_texline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) - - # print(len(contours_only_text_parent_h),len(contours_only_text_parent_h_d_ordered),'contours_only_text_parent_h') - pixel_lines = 6 - - - if not self.headers_off: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + if self.full_layout: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + self.logger.info("Job done in %.1fs", time.time() - t0) + ##return pcgts + else: + contours_only_text_parent_h = None if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - # print(peaks_neg_fin,peaks_neg_fin_d,'num_col2') - # print(splitter_y_new,splitter_y_new_d,'num_col_classifier') - # print(matrix_of_lines_ch.shape,matrix_of_lines_ch_d.shape,'matrix_of_lines_ch') - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - #regions_without_separators_0 = regions_without_separators[:, :].sum(axis=0) - #meda_n_updown = regions_without_separators_0[len(regions_without_separators_0) :: -1] - #first_nonzero = next((i for i, x in enumerate(regions_without_separators_0) if x), 0) - #last_nonzero = next((i for i, x in enumerate(meda_n_updown) if x), 0) - #last_nonzero = len(regions_without_separators_0) - last_nonzero - - #random_pixels_for_image = np.random.randn(regions_without_separators.shape[0], regions_without_separators.shape[1]) - #random_pixels_for_image[random_pixels_for_image < -0.5] = 0 - #random_pixels_for_image[random_pixels_for_image != 0] = 1 - #regions_without_separators[(random_pixels_for_image[:, :] == 1) & (text_regions_p[:, :] == 5)] = 1 - - #regions_without_separators[:, 0:first_nonzero] = 0 - #regions_without_separators[:, last_nonzero:] = 0 - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - #regions_without_separators_0 = regions_without_separators_d[:, :].sum(axis=0) - #meda_n_updown = regions_without_separators_0[len(regions_without_separators_0) :: -1] - #first_nonzero = next((i for i, x in enumerate(regions_without_separators_0) if x), 0) - #last_nonzero = next((i for i, x in enumerate(meda_n_updown) if x), 0) - #last_nonzero = len(regions_without_separators_0) - last_nonzero - - #random_pixels_for_image = np.random.randn(regions_without_separators_d.shape[0], regions_without_separators_d.shape[1]) - #random_pixels_for_image[random_pixels_for_image < -0.5] = 0 - #random_pixels_for_image[random_pixels_for_image != 0] = 1 - ##regions_without_separators_d[(random_pixels_for_image[:, :] == 1) & (text_regions_p_1_n[:, :] == 5)] = 1 - - #regions_without_separators_d[:, 0:first_nonzero] = 0 - #regions_without_separators_d[:, last_nonzero:] = 0 - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) - - if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - - if self.full_layout: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) - self.logger.info("Job done in %.1fs", time.time() - t0) - return pcgts - else: - contours_only_text_parent_h = None - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) - self.logger.info("Job done in %.1fs", time.time() - t0) - return pcgts + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) + self.logger.info("Job done in %.1fs", time.time() - t0) + ##return pcgts + self.writer.write_pagexml(pcgts) + #self.logger.info("Job done in %.1fs", time.time() - t0) + self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 2533455..da14139 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -797,6 +797,76 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch): return layout_in_patch def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_texline_polygons,slopes,contours_only_text_parent_d_ordered): + + cx_main,cy_main ,x_min_main , x_max_main, y_min_main ,y_max_main,y_corr_x_min_from_argmin=find_new_features_of_contours(contours_only_text_parent) + + length_con=x_max_main-x_min_main + height_con=y_max_main-y_min_main + + + + all_found_texline_polygons_main=[] + all_found_texline_polygons_head=[] + + all_box_coord_main=[] + all_box_coord_head=[] + + slopes_main=[] + slopes_head=[] + + contours_only_text_parent_main=[] + contours_only_text_parent_head=[] + + contours_only_text_parent_main_d=[] + contours_only_text_parent_head_d=[] + + for ii in range(len(contours_only_text_parent)): + con=contours_only_text_parent[ii] + img=np.zeros((regions_model_1.shape[0],regions_model_1.shape[1],3)) + img = cv2.fillPoly(img, pts=[con], color=(255, 255, 255)) + + + + all_pixels=((img[:,:,0]==255)*1).sum() + + pixels_header=( ( (img[:,:,0]==255) & (regions_model_full[:,:,0]==2) )*1 ).sum() + pixels_main=all_pixels-pixels_header + + + if (pixels_header>=pixels_main) and ( (length_con[ii]/float(height_con[ii]) )>=1.3 ): + regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=2 + contours_only_text_parent_head.append(con) + if contours_only_text_parent_d_ordered is not None: + contours_only_text_parent_head_d.append(contours_only_text_parent_d_ordered[ii]) + all_box_coord_head.append(all_box_coord[ii]) + slopes_head.append(slopes[ii]) + all_found_texline_polygons_head.append(all_found_texline_polygons[ii]) + else: + regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=1 + contours_only_text_parent_main.append(con) + if contours_only_text_parent_d_ordered is not None: + contours_only_text_parent_main_d.append(contours_only_text_parent_d_ordered[ii]) + all_box_coord_main.append(all_box_coord[ii]) + slopes_main.append(slopes[ii]) + all_found_texline_polygons_main.append(all_found_texline_polygons[ii]) + + #print(all_pixels,pixels_main,pixels_header) + + return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_texline_polygons_main,all_found_texline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d + + +def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_texline_polygons,slopes,contours_only_text_parent_d_ordered): + + ### to make it faster + h_o = regions_model_1.shape[0] + w_o = regions_model_1.shape[1] + + regions_model_1 = cv2.resize(regions_model_1, (int(regions_model_1.shape[1]/3.), int(regions_model_1.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) + regions_model_full = cv2.resize(regions_model_full, (int(regions_model_full.shape[1]/3.), int(regions_model_full.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) + contours_only_text_parent = [ (i/3.).astype(np.int32) for i in contours_only_text_parent] + + ### + cx_main,cy_main ,x_min_main , x_max_main, y_min_main ,y_max_main,y_corr_x_min_from_argmin=find_new_features_of_contours(contours_only_text_parent) length_con=x_max_main-x_min_main @@ -853,8 +923,14 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions - #plt.imshow(img[:,:,0]) - #plt.show() + ### to make it faster + + regions_model_1 = cv2.resize(regions_model_1, (w_o, h_o), interpolation=cv2.INTER_NEAREST) + #regions_model_full = cv2.resize(img, (int(regions_model_full.shape[1]/3.), int(regions_model_full.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) + contours_only_text_parent_head = [ (i*3.).astype(np.int32) for i in contours_only_text_parent_head] + contours_only_text_parent_main = [ (i*3.).astype(np.int32) for i in contours_only_text_parent_main] + ### + return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_texline_polygons_main,all_found_texline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col): diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index 6b81391..b29b5b6 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -3,7 +3,8 @@ import numpy as np from shapely import geometry from .rotate import rotate_image, rotation_image_new - +from multiprocessing import Process, Queue, cpu_count +from multiprocessing import Pool def contours_in_same_horizon(cy_main_hor): X1 = np.zeros((len(cy_main_hor), len(cy_main_hor))) X2 = np.zeros((len(cy_main_hor), len(cy_main_hor))) @@ -147,6 +148,96 @@ def return_contours_of_interested_region(region_pre_p, pixel, min_area=0.0002): return contours_imgs +def do_work_of_contours_in_image(queue_of_all_params, contours_per_process, indexes_r_con_per_pro, img, slope_first): + cnts_org_per_each_subprocess = [] + index_by_text_region_contours = [] + for mv in range(len(contours_per_process)): + index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) + + img_copy = np.zeros(img.shape) + img_copy = cv2.fillPoly(img_copy, pts=[contours_per_process[mv]], color=(1, 1, 1)) + + img_copy = rotation_image_new(img_copy, -slope_first) + + img_copy = img_copy.astype(np.uint8) + imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) + cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) + + + cnts_org_per_each_subprocess.append(cont_int[0]) + + queue_of_all_params.put([ cnts_org_per_each_subprocess, index_by_text_region_contours]) + + +def get_textregion_contours_in_org_image_multi(cnts, img, slope_first): + + num_cores = cpu_count() + queue_of_all_params = Queue() + + processes = [] + nh = np.linspace(0, len(cnts), num_cores + 1) + indexes_by_text_con = np.array(range(len(cnts))) + for i in range(num_cores): + contours_per_process = cnts[int(nh[i]) : int(nh[i + 1])] + indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] + + processes.append(Process(target=do_work_of_contours_in_image, args=(queue_of_all_params, contours_per_process, indexes_text_con_per_process, img,slope_first ))) + for i in range(num_cores): + processes[i].start() + cnts_org = [] + all_index_text_con = [] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + contours_for_sub_process = list_all_par[0] + indexes_for_sub_process = list_all_par[1] + for j in range(len(contours_for_sub_process)): + cnts_org.append(contours_for_sub_process[j]) + all_index_text_con.append(indexes_for_sub_process[j]) + for i in range(num_cores): + processes[i].join() + + print(all_index_text_con) + return cnts_org +def loop_contour_image(index_l, cnts,img, slope_first): + img_copy = np.zeros(img.shape) + img_copy = cv2.fillPoly(img_copy, pts=[cnts[index_l]], color=(1, 1, 1)) + + # plt.imshow(img_copy) + # plt.show() + + # print(img.shape,'img') + img_copy = rotation_image_new(img_copy, -slope_first) + ##print(img_copy.shape,'img_copy') + # plt.imshow(img_copy) + # plt.show() + + img_copy = img_copy.astype(np.uint8) + imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) + cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) + # print(np.shape(cont_int[0])) + return cont_int[0] + +def get_textregion_contours_in_org_image_multi2(cnts, img, slope_first): + + cnts_org = [] + # print(cnts,'cnts') + with Pool(cpu_count()) as p: + cnts_org = p.starmap(loop_contour_image, [(index_l,cnts, img,slope_first) for index_l in range(len(cnts))]) + + print(len(cnts_org),'lendiha') + + return cnts_org + def get_textregion_contours_in_org_image(cnts, img, slope_first): cnts_org = [] @@ -175,11 +266,43 @@ def get_textregion_contours_in_org_image(cnts, img, slope_first): # print(np.shape(cont_int[0])) cnts_org.append(cont_int[0]) - # print(cnts_org,'cnts_org') + return cnts_org + +def get_textregion_contours_in_org_image_light(cnts, img, slope_first): + + h_o = img.shape[0] + w_o = img.shape[1] + + img = cv2.resize(img, (int(img.shape[1]/3.), int(img.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) + ##cnts = list( (np.array(cnts)/2).astype(np.int16) ) + #cnts = cnts/2 + cnts = [(i/ 3).astype(np.int32) for i in cnts] + cnts_org = [] + #print(cnts,'cnts') + for i in range(len(cnts)): + img_copy = np.zeros(img.shape) + img_copy = cv2.fillPoly(img_copy, pts=[cnts[i]], color=(1, 1, 1)) + + # plt.imshow(img_copy) + # plt.show() + + # print(img.shape,'img') + img_copy = rotation_image_new(img_copy, -slope_first) + ##print(img_copy.shape,'img_copy') + # plt.imshow(img_copy) + # plt.show() + + img_copy = img_copy.astype(np.uint8) + imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) + cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) + # print(np.shape(cont_int[0])) + cnts_org.append(cont_int[0]*3) - # sys.exit() - # self.y_shift = np.abs(img_copy.shape[0] - img.shape[0]) - # self.x_shift = np.abs(img_copy.shape[1] - img.shape[1]) return cnts_org def return_contours_of_interested_textline(region_pre_p, pixel): From 5dafa2095bbfb0aafb151a24fcab1afdd983d65f Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 29 Mar 2022 17:18:12 +0200 Subject: [PATCH 025/412] use
instead of wiki --- README.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a9a86e8..a35f972 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,127 @@ eynollah \ The tool performs better with RGB images than greyscale/binarized images. -Additional documentation can be found in the [wiki](https://github.com/qurator-spk/eynollah/wiki). +## Documentation + +
+ click to expand/collapse + +## Region types + +
+ click to expand/collapse
+ +Eynollah can currently be used to detect the following region types/elements: +* [Border](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_BorderType.html) +* [Textregion](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html) +* [Textline](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html) +* [Image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html) +* [Separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html) +* [Marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html) +* [Initial (Drop Capital)](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html) + +In addition, the tool can detect the [ReadingOrder](https://ocr-d.de/en/gt-guidelines/trans/lyLeserichtung.html) of regions. The final goal is to feed the output to an OCR model. + +
+ +## Method description + +
+ click to expand/collapse
+ +Eynollah uses a combination of various models and heuristics (see flowchart below for the different stages and how they interact): +* [Border detection](https://github.com/qurator-spk/eynollah#border-detection) +* [Layout detection](https://github.com/qurator-spk/eynollah#layout-detection) +* [Textline detection](https://github.com/qurator-spk/eynollah#textline-detection) +* [Image enhancement](https://github.com/qurator-spk/eynollah#Image_enhancement) +* [Scale classification](https://github.com/qurator-spk/eynollah#Scale_classification) +* [Heuristic methods](https://https://github.com/qurator-spk/eynollah#heuristic-methods) + +The first three stages are based on [pixel-wise segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + +![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) + +### Border detection +For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. + +### Layout detection +As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. + +### Textline detection +In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. + +### Image enhancement +This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. + +### Scale classification +This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. + +### Heuristic methods +Some heuristic methods are also employed to further improve the model predictions: +* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. +* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. +* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. +* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. +* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. +* Finally, using the derived coordinates, bounding boxes are determined for each textline. + +
+ +## Model description + +
+ click to expand/collapse
+ +TODO + +
+ +## How to use + +
+ click to expand/collapse
+ +First, this model makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. + +* If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. + +* If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi (pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. + +* For some documents, while the quality is good, their scale is very large, and the performance of tool decreases. In such cases you can set `-as` (**a**llow **s**caling) to `true`. With this option enabled, the tool will try to rescale the image and only then the layout detection process will begin. + +* If you care about drop capitals (initials) and headings, you can set `-fl` (**f**ull **l**ayout) to `true`. With this setting, the tool can currently distinguish 7 document layout classes/elements. + +* In cases where the document includes curved headers or curved lines, rectangular bounding boxes for textlines will not be a great option. In such cases it is strongly recommended setting the flag `-cl` (**c**urved **l**ines) to `true` to find contours of curved lines instead of rectangular bounding boxes. Be advised that enabling this option increases the processing time of the tool. + +* To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide a directory path to store the extracted images. + +* This tool is actively being developed. If problems occur, or the performance does not meet your expectations, we welcome your feedback via [issues](https://github.com/qurator-spk/eynollah/issues). + +### `--full-layout` vs `--no-full-layout` + +Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: + +| | `--full-layout` | `--no-full-layout` | +| --- | --- | --- | +| reading order | x | x | +| header regions | x | - | +| text regions | x | x | +| text regions / text line | x | x | +| drop-capitals | x | - | +| marginals | x | x | +| marginals / text line | x | x | +| image region | x | x | + +### Use as OCR-D processor + +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input (the image provided by `@imageFilename` is passed on directly): + +`ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models` + + ### Eynollah "light" + + TODO + +
+ +
From aa64a54feb45de5086b1cd888ee1e15089631b73 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 29 Mar 2022 17:19:18 +0200 Subject: [PATCH 026/412] markdown --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a35f972..deedcf7 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The tool performs better with RGB images than greyscale/binarized images.
click to expand/collapse -## Region types +### Region types
click to expand/collapse
@@ -73,7 +73,7 @@ In addition, the tool can detect the [ReadingOrder](https://ocr-d.de/en/gt-guide
-## Method description +### Method description
click to expand/collapse
@@ -90,19 +90,19 @@ The first three stages are based on [pixel-wise segmentation](https://github.com ![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) -### Border detection +#### Border detection For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. ### Layout detection As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. -### Textline detection +#### Textline detection In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. -### Image enhancement +#### Image enhancement This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. -### Scale classification +#### Scale classification This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. ### Heuristic methods @@ -116,7 +116,7 @@ Some heuristic methods are also employed to further improve the model prediction
-## Model description +### Model description
click to expand/collapse
@@ -125,7 +125,7 @@ TODO
-## How to use +### How to use
click to expand/collapse
@@ -146,7 +146,7 @@ First, this model makes use of up to 9 trained models which are responsible for * This tool is actively being developed. If problems occur, or the performance does not meet your expectations, we welcome your feedback via [issues](https://github.com/qurator-spk/eynollah/issues). -### `--full-layout` vs `--no-full-layout` +#### `--full-layout` vs `--no-full-layout` Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: @@ -161,13 +161,13 @@ Here are the difference in elements detected depending on the `--full-layout`/`- | marginals / text line | x | x | | image region | x | x | -### Use as OCR-D processor +#### Use as OCR-D processor Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input (the image provided by `@imageFilename` is passed on directly): `ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models` - ### Eynollah "light" + #### Eynollah "light" TODO From 441c8566dda5cc2b37fd92a39236dc595a547298 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 30 Mar 2022 17:05:04 +0200 Subject: [PATCH 027/412] additional details on OCR-D usage --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index deedcf7..b20289d 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,15 @@ Here are the difference in elements detected depending on the `--full-layout`/`- #### Use as OCR-D processor -Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input (the image provided by `@imageFilename` is passed on directly): +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input like this: `ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models` + +In fact, the image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. calling + +`ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models` + +would still use the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps #### Eynollah "light" From 2eacb9a8ec4ba1d2150e3c984d6b0b8678a07cd9 Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 4 Apr 2022 20:34:59 -0400 Subject: [PATCH 028/412] renaming the models --- qurator/eynollah/eynollah.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index b3fca7b..c980866 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -144,18 +144,18 @@ class Eynollah: self.logger = logger if logger else getLogger('eynollah') self.dir_models = dir_models - self.model_dir_of_enhancement = dir_models + "/model_enhancement.h5" - self.model_dir_of_binarization = dir_models + "/model_bin_sbb_ens.h5" - self.model_dir_of_col_classifier = dir_models + "/model_scale_classifier.h5" - self.model_region_dir_p = dir_models + "/model_main_covid19_lr5-5_scale_1_1_great.h5" - self.model_region_dir_p2 = dir_models + "/model_main_home_corona3_rot.h5" - self.model_region_dir_fully_np = dir_models + "/model_no_patches_class0_30eopch.h5" - self.model_region_dir_fully = dir_models + "/model_3up_new_good_no_augmentation.h5" - self.model_page_dir = dir_models + "/model_page_mixed_best.h5" - self.model_region_dir_p_ens = dir_models + "/model_ensemble_s.h5" - self.model_region_dir_p_ens_light = dir_models + "/model_11.h5" - self.model_textline_dir = dir_models + "/model_textline_newspapers.h5" - self.model_tables = dir_models + "/model_tables_ens_mixed_new_2.h5" + self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425.h5" + self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425.h5" + self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425.h5" + self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425.h5" + self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425.h5" + self.model_region_dir_fully_np = dir_models + "/eynollah-full-regions-1column_20210425.h5" + self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425.h5" + self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425.h5" + self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425.h5" + self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314.h5" + self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" + self.model_tables = dir_models + "/eynollah-tables_20210319.h5" if dir_in and light_version: config = tf.compat.v1.ConfigProto() From 8d19c4c6320419125f3e178a5d990217522fd667 Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 4 Apr 2022 20:45:12 -0400 Subject: [PATCH 029/412] updating readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7673954..506e93d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ eynollah \ -ho \ -sl \ -ep +-light +-di ``` From 94c3b0fc286c6c7b051eea2501ca2478a6d118c4 Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 4 Apr 2022 20:48:21 -0400 Subject: [PATCH 030/412] updating readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 506e93d..afafb3a 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ eynollah \ -ho \ -sl \ -ep --light --di +-light +-di ``` From e564451861d9c6ee3db5e859d87b14354a6db44c Mon Sep 17 00:00:00 2001 From: vahid Date: Mon, 4 Apr 2022 21:13:21 -0400 Subject: [PATCH 031/412] updating readme --- README.md | 188 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 119 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index afafb3a..0232db7 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,8 @@ # Eynollah -> Document Layout Analysis +> Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) -## Introduction -This tool performs document layout analysis (segmentation) from image data and returns the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). - -It can currently detect the following layout classes/elements: -* [Border](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_BorderType.html) -* [Textregion](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html) -* [Textline](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html) -* [Image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html) -* [Separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html) -* [Marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html) -* [Initial (Drop Capital)](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html) - -In addition, the tool can be used to detect the _[ReadingOrder](https://ocr-d.de/en/gt-guidelines/trans/lyLeserichtung.html)_ of regions. The final goal is to feed the output to an OCR model. - -The tool uses a combination of various models and heuristics (see flowchart below for the different stages and how they interact): -* [Border detection](https://github.com/qurator-spk/eynollah#border-detection) -* [Layout detection](https://github.com/qurator-spk/eynollah#layout-detection) -* [Textline detection](https://github.com/qurator-spk/eynollah#textline-detection) -* [Image enhancement](https://github.com/qurator-spk/eynollah#Image_enhancement) -* [Scale classification](https://github.com/qurator-spk/eynollah#Scale_classification) -* [Heuristic methods](https://https://github.com/qurator-spk/eynollah#heuristic-methods) - -The first three stages are based on [pixel-wise segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). - -![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) - -## Border detection -For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. - -## Layout detection -As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. - -## Textline detection -In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. - -## Image enhancement -This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. - -## Scale classification -This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. - -## Heuristic methods -Some heuristic methods are also employed to further improve the model predictions: -* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. -* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. -* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. -* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. -* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. -* Finally, using the derived coordinates, bounding boxes are determined for each textline. - -## Light version -layout detection is implemented in lower scale and with only one model. - ## Installation `pip install .` or @@ -69,13 +16,17 @@ Alternatively, you can also use `make` with these targets: ### Models -In order to run this tool you also need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). +In order to run this tool you need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). Alternatively, running `make models` will download and extract models to `$(PWD)/models_eynollah`. +### Training + +In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + ## Usage -The basic command-line interface can be called like this: +The command-line interface can be called like this: ```sh eynollah \ @@ -99,25 +50,88 @@ eynollah \ ``` -The tool does accept and works better on original images (RGB format) than binarized images. +The tool performs better with RGB images than greyscale/binarized images. -### `--full-layout` vs `--no-full-layout` +## Documentation + +
+ click to expand/collapse -Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: +### Region types -| | `--full-layout` | `--no-full-layout` | -| --- | --- | --- | -| reading order | x | x | -| header regions | x | - | -| text regions | x | x | -| text regions / text line | x | x | -| drop-capitals | x | - | -| marginals | x | x | -| marginals / text line | x | x | -| image region | x | x | +
+ click to expand/collapse
+ +Eynollah can currently be used to detect the following region types/elements: +* [Border](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_BorderType.html) +* [Textregion](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html) +* [Textline](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html) +* [Image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html) +* [Separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html) +* [Marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html) +* [Initial (Drop Capital)](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html) + +In addition, the tool can detect the [ReadingOrder](https://ocr-d.de/en/gt-guidelines/trans/lyLeserichtung.html) of regions. The final goal is to feed the output to an OCR model. + +
+### Method description + +
+ click to expand/collapse
+ +Eynollah uses a combination of various models and heuristics (see flowchart below for the different stages and how they interact): +* [Border detection](https://github.com/qurator-spk/eynollah#border-detection) +* [Layout detection](https://github.com/qurator-spk/eynollah#layout-detection) +* [Textline detection](https://github.com/qurator-spk/eynollah#textline-detection) +* [Image enhancement](https://github.com/qurator-spk/eynollah#Image_enhancement) +* [Scale classification](https://github.com/qurator-spk/eynollah#Scale_classification) +* [Heuristic methods](https://https://github.com/qurator-spk/eynollah#heuristic-methods) + +The first three stages are based on [pixel-wise segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + +![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) + +#### Border detection +For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. + +### Layout detection +As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. + +#### Textline detection +In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. + +#### Image enhancement +This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. + +#### Scale classification +This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. + +### Heuristic methods +Some heuristic methods are also employed to further improve the model predictions: +* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. +* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. +* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. +* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. +* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. +* Finally, using the derived coordinates, bounding boxes are determined for each textline. + +
+ +### Model description + +
+ click to expand/collapse
+ +TODO + +
+ ### How to use +
+ click to expand/collapse
+ First, this model makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. * If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. @@ -133,3 +147,39 @@ First, this model makes use of up to 9 trained models which are responsible for * To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide a directory path to store the extracted images. * This tool is actively being developed. If problems occur, or the performance does not meet your expectations, we welcome your feedback via [issues](https://github.com/qurator-spk/eynollah/issues). + +#### `--full-layout` vs `--no-full-layout` + +Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: + +| | `--full-layout` | `--no-full-layout` | +| --- | --- | --- | +| reading order | x | x | +| header regions | x | - | +| text regions | x | x | +| text regions / text line | x | x | +| drop-capitals | x | - | +| marginals | x | x | +| marginals / text line | x | x | +| image region | x | x | + +#### Use as OCR-D processor + +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input like this: + +`ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models` + +In fact, the image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. calling + +`ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models` + +would still use the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps + + #### Eynollah "light" + + TODO + +
+ +
+ From d19170035d36e9f0478dd9dfeb781bd55e017171 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 4 Apr 2022 22:21:55 -0400 Subject: [PATCH 032/412] updating model directory --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 920f15b..39c8a9b 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ models_eynollah: models_eynollah.tar.gz tar xf models_eynollah.tar.gz models_eynollah.tar.gz: - wget 'https://qurator-data.de/eynollah/models_eynollah.tar.gz' + wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # Install with pip install: From adf10942fa8516bbab5bd01649944f9613c24c96 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 5 Apr 2022 07:47:55 -0400 Subject: [PATCH 033/412] issue #55 resolved --- qurator/eynollah/eynollah.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 81c0b0c..d3253a6 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -546,7 +546,7 @@ class Eynollah: if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) - self.logger.info("Image dimensions: %sx%s", img_height_model, img_width_model) + self.logger.info("Patch size: %sx%s", img_height_model, img_width_model) margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin @@ -2316,7 +2316,7 @@ class Eynollah: num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - self.logger.info('cont_page %s', cont_page) + #self.logger.info('cont_page %s', cont_page) if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") @@ -2355,7 +2355,7 @@ class Eynollah: if len(contours_only_text_parent) > 0: areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - self.logger.info('areas_cnt_text %s', areas_cnt_text) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] @@ -2445,7 +2445,7 @@ class Eynollah: cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) else: From f27ac155ae1362625f1d5fce9ffcb354ac6c4c30 Mon Sep 17 00:00:00 2001 From: "Gerber, Mike" Date: Wed, 6 Apr 2022 14:47:29 +0200 Subject: [PATCH 034/412] =?UTF-8?q?=F0=9F=A7=B9=20Downgrade=20"Patch=20siz?= =?UTF-8?q?e"=20log=20message=20to=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes gh-55. --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index d3253a6..f8d7d09 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -546,7 +546,7 @@ class Eynollah: if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) - self.logger.info("Patch size: %sx%s", img_height_model, img_width_model) + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin From a33a1995cb880d190b62cd9593dac3dd43d33deb Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 8 Apr 2022 16:31:20 +0200 Subject: [PATCH 035/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b20289d..9cb5d60 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Some heuristic methods are also employed to further improve the model prediction
click to expand/collapse
-TODO +Coming soon
From 3871e22c358093cb408fac68e8f62ff575658dd9 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 12 Apr 2022 00:58:20 +0200 Subject: [PATCH 036/412] how the models are trained --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0232db7..7438427 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,24 @@ Some heuristic methods are also employed to further improve the model prediction
click to expand/collapse
-TODO +#### Enhancement model: +The image enhancement model is again an image-to-image model, trained on document images with low quality and GT of corresponding images with higher quality. For training the image enhancement model, a total of 1127 document images underwent 11 different downscaling processes and consequently 11 different qualities for each image were derived. The resulting images were cropped into patches of 672*672 pixels. Adam is used as an optimizer and the learning rate is 1e-4. Scaling is the only augmentation applied for training. The model is trained with a batch size of 2 and for 5 epochs. + +#### Classifier model: +In order to obtain high quality results, it is beneficial to scale the document image to the same scale of the images in the training dataset that the models were trained on. The classifier model predicts the number of columns in a document by creating a training set for that purpose with manual classification of all documents into six classes with either one, two, three, four, five, or six and more columns respectively. Classifier model is a ResNet50+2 dense layers on top. The input size of model is 448*448 and Adam is used as an optimizer and the learning rate is 1e-4. Model is trained for 300 epochs. + +#### Page extractor model: +This a deep learning model which helps to crop the page borders by using a pixel-wise segmentation method. In case of page extraction it is necessary to train the model on the entire (document) image, i.e. full images are resized to the input size of the model (no patches). For training, the model is fed with entire images from the 2820 samples of the extended training set. The input size of the the page extraction model is 448*448 pixels. Adam is used as an optimizer and the learning rate is 1e-6. The model is trained with a batch size of 4 and for 30 epochs. + +#### Early layout model: +The early layout detection model detects only the main and recursive regions in a document like background, text regions, separators and images. In the case of early layout segmentation, we used 381 pages to train the model. The model is fed with patches of size 448*672 pixels. Adam is used as an optimizer and the learning rate is 1e-4. Two models were trained, one with scale augmentation and another one without any augmentation. Both models were trained for 12 epochs and with a batch size of 3. Categorical cross entropy is used as a loss function. + +#### Full layout model: +By full layout detection we have added two more elements of a document structure, drop capitals and headings, onto early layout elements. For the secondary layout segmentation we have trained two models. One is trained with 355 pages containing 3 or more columns and in patches with a size of 896*896 pixels. The other model is trained on 634 pages that have only one column. The second model is fed with the entire image with input size +of 896 * 896 pixels (not in patches). Adam is used as an optimizer and the learning rate is 1e-4. Then both models are trained for 8 epochs with a batch size of 1. Soft dice is used as the loss function. + +#### Text line segmentation model: +For text line segmentation, 342 pages were used for training. The model is trained in patches with the size of 448*672. Adam is used as an optimizer and the learning rate is 1e-4. The training set is augmented with scaling and rotation. The model is trained only for 1 epoch with a batch size of 3. Soft dice is again used as the loss function.
@@ -177,7 +194,7 @@ would still use the original (RGB) image despite any binarization that may have #### Eynollah "light" - TODO + Eynollah light has used a faster method to predict and extract early layout. On other hand with light version deskewing is not applied for any text region and in return it is done for the whole document once. The other option that users have with light version is that instead of image name a folder of images can be given as input and in this case all models will be loaded and then processing for all images will be implemented. This step accelerates process of document analysis.
From 3bbbeecfec20ec5a5b1d0113a1a2621e056f2bb3 Mon Sep 17 00:00:00 2001 From: vahid Date: Wed, 20 Apr 2022 15:31:10 +0200 Subject: [PATCH 037/412] all options are enabled for light version --- qurator/eynollah/eynollah.py | 39 +++++++++++++++++++++++++----------- qurator/eynollah/plot.py | 8 ++++---- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c980866..48a640c 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -119,6 +119,12 @@ class Eynollah: self.image_filename = image_filename self.dir_out = dir_out self.dir_in = dir_in + self.dir_of_all = dir_of_all + self.dir_of_deskewed = dir_of_deskewed + self.dir_of_deskewed = dir_of_deskewed + self.dir_of_cropped_images=dir_of_cropped_images + self.dir_of_layout=dir_of_layout + self.enable_plotting = enable_plotting self.allow_enhancement = allow_enhancement self.curved_line = curved_line self.full_layout = full_layout @@ -128,14 +134,14 @@ class Eynollah: self.headers_off = headers_off self.light_version = light_version self.pcgts = pcgts - self.plotter = None if not enable_plotting else EynollahPlotter( - dir_out=self.dir_out, - dir_of_all=dir_of_all, - dir_of_deskewed=dir_of_deskewed, - dir_of_cropped_images=dir_of_cropped_images, - dir_of_layout=dir_of_layout, - image_filename_stem=Path(Path(image_filename).name).stem) if not dir_in: + self.plotter = None if not enable_plotting else EynollahPlotter( + dir_out=self.dir_out, + dir_of_all=dir_of_all, + dir_of_deskewed=dir_of_deskewed, + dir_of_cropped_images=dir_of_cropped_images, + dir_of_layout=dir_of_layout, + image_filename_stem=Path(Path(image_filename).name).stem) self.writer = EynollahXmlWriter( dir_out=self.dir_out, image_filename=self.image_filename, @@ -208,6 +214,14 @@ class Eynollah: self._imgs = self._cache_images(image_filename=image_filename) self.image_filename = image_filename + self.plotter = None if not self.enable_plotting else EynollahPlotter( + dir_out=self.dir_out, + dir_of_all=self.dir_of_all, + dir_of_deskewed=self.dir_of_deskewed, + dir_of_cropped_images=self.dir_of_cropped_images, + dir_of_layout=self.dir_of_layout, + image_filename_stem=Path(Path(image_filename).name).stem) + self.writer = EynollahXmlWriter( dir_out=self.dir_out, image_filename=self.image_filename, @@ -1396,7 +1410,7 @@ class Eynollah: queue_of_all_params.put([textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours, slopes_per_each_subprocess]) def do_work_of_slopes_new_light(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, indexes_r_con_per_pro, image_page_rotated, slope_deskew): - self.logger.debug('enter do_work_of_slopes_new') + self.logger.debug('enter do_work_of_slopes_new_light') slopes_per_each_subprocess = [] bounding_box_of_textregion_per_each_subprocess = [] textlines_rectangles_per_each_subprocess = [] @@ -1566,8 +1580,8 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) - def get_regions_from_xy_2models_light(self,img,is_image_enhanced, num_col_classifier): - self.logger.debug("enter get_regions_from_xy_2models") + def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): + self.logger.debug("enter get_regions_light_v") erosion_hurts = False img_org = np.copy(img) img_height_h = img_org.shape[0] @@ -2929,7 +2943,7 @@ class Eynollah: t1 = time.time() if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_from_xy_2models_light(img_res, is_image_enhanced, num_col_classifier) + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ @@ -3179,4 +3193,5 @@ class Eynollah: ##return pcgts self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) - self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) + if self.dir_in: + self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/qurator/eynollah/plot.py b/qurator/eynollah/plot.py index b22c8f1..ec4e290 100644 --- a/qurator/eynollah/plot.py +++ b/qurator/eynollah/plot.py @@ -74,8 +74,8 @@ class EynollahPlotter(): if self.dir_of_layout is not None: values = np.unique(text_regions_p[:, :]) # pixels=['Background' , 'Main text' , 'Heading' , 'Marginalia' ,'Drop capitals' , 'Images' , 'Seperators' , 'Tables', 'Graphics'] - pixels = ["Background", "Main text", "Header", "Marginalia", "Drop capital", "Image", "Separator"] - values_indexes = [0, 1, 2, 8, 4, 5, 6] + pixels = ["Background", "Main text", "Header", "Marginalia", "Drop capital", "Image", "Separator", "Tables"] + values_indexes = [0, 1, 2, 8, 4, 5, 6, 10] plt.figure(figsize=(40, 40)) plt.rcParams["font.size"] = "40" im = plt.imshow(text_regions_p[:, :]) @@ -88,8 +88,8 @@ class EynollahPlotter(): if self.dir_of_all is not None: values = np.unique(text_regions_p[:, :]) # pixels=['Background' , 'Main text' , 'Heading' , 'Marginalia' ,'Drop capitals' , 'Images' , 'Seperators' , 'Tables', 'Graphics'] - pixels = ["Background", "Main text", "Header", "Marginalia", "Drop capital", "Image", "Separator"] - values_indexes = [0, 1, 2, 8, 4, 5, 6] + pixels = ["Background", "Main text", "Header", "Marginalia", "Drop capital", "Image", "Separator", "Tables"] + values_indexes = [0, 1, 2, 8, 4, 5, 6, 10] plt.figure(figsize=(80, 40)) plt.rcParams["font.size"] = "40" plt.subplot(1, 2, 1) From 568391ec4ad0af096f5f2168d448331d5b9622f2 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:54:20 +0200 Subject: [PATCH 038/412] require model command line option (fix #59) (#73) --- qurator/eynollah/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index f343918..e419411 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -24,6 +24,7 @@ from qurator.eynollah.eynollah import Eynollah "-m", help="directory of models", type=click.Path(exists=True, file_okay=False), + required=True, ) @click.option( "--save_images", From ecf117ca9596e570dfb5155abbd8966b1e82a486 Mon Sep 17 00:00:00 2001 From: cneud Date: Tue, 26 Apr 2022 11:50:20 +0200 Subject: [PATCH 039/412] adapt to tf1.compat session mode in tf2 --- qurator/eynollah/eynollah.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index f8d7d09..784f07f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -20,10 +20,10 @@ import numpy as np os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" stderr = sys.stderr sys.stderr = open(os.devnull, "w") -from keras import backend as K -from keras.models import load_model -sys.stderr = stderr import tensorflow as tf +from tensorflow.python.keras import backend as K +from tensorflow.keras.models load_model +sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") from scipy.signal import find_peaks From 8c11b2253dfad142aa896defc70813f222a713b7 Mon Sep 17 00:00:00 2001 From: cneud Date: Tue, 26 Apr 2022 11:51:22 +0200 Subject: [PATCH 040/412] update requirements (use tf2) --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8520780..6f2ea48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ # ocrd includes opencv, numpy, shapely, click ocrd >= 2.23.3 -keras >= 2.3.1, < 2.4 scikit-learn >= 0.23.2 -tensorflow-gpu >= 1.15, < 2 +tensorflow-gpu >= 2.4.0 imutils >= 0.5.3 matplotlib setuptools >= 50 From 934bbd589267d08ff1386315868b752a631cbd42 Mon Sep 17 00:00:00 2001 From: cneud Date: Tue, 26 Apr 2022 12:04:27 +0200 Subject: [PATCH 041/412] cleanup --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 784f07f..ff3ceac 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -22,7 +22,7 @@ stderr = sys.stderr sys.stderr = open(os.devnull, "w") import tensorflow as tf from tensorflow.python.keras import backend as K -from tensorflow.keras.models load_model +from tensorflow.keras.models import load_model sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") From 735abc43f3102e8cf35d71dff2daeffc4a1cfeac Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 28 Apr 2022 01:14:57 +0200 Subject: [PATCH 042/412] option to ignore page extraction --- qurator/eynollah/cli.py | 8 ++ qurator/eynollah/eynollah.py | 240 +++++++++++++---------------------- 2 files changed, 97 insertions(+), 151 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index ca938c4..18ea583 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -108,6 +108,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="if this parameter set to true, this tool would use lighter version", ) +@click.option( + "--ignore_page_extraction/--extract_page_included", + "-ipe/-epi", + is_flag=True, + help="if this parameter set to true, this tool would ignore page extraction", +) @click.option( "--log-level", "-l", @@ -132,6 +138,7 @@ def main( allow_scaling, headers_off, light_version, + ignore_page_extraction, log_level ): if log_level: @@ -161,6 +168,7 @@ def main( allow_scaling=allow_scaling, headers_off=headers_off, light_version=light_version, + ignore_page_extraction=ignore_page_extraction, ) eynollah.run() #pcgts = eynollah.run() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 48a640c..8957248 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -105,6 +105,7 @@ class Eynollah: allow_scaling=False, headers_off=False, light_version=False, + ignore_page_extraction=False, override_dpi=None, logger=None, pcgts=None, @@ -133,6 +134,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.light_version = light_version + self.ignore_page_extraction = ignore_page_extraction self.pcgts = pcgts if not dir_in: self.plotter = None if not enable_plotting else EynollahPlotter( @@ -886,169 +888,100 @@ class Eynollah: gc.collect() return prediction_true - def early_page_for_num_of_column_classification(self,img_bin): - self.logger.debug("enter early_page_for_num_of_column_classification") - if self.input_binary: - img =np.copy(img_bin) - img = img.astype(np.uint8) - else: - img = self.imread() - if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(img, (5, 5), 0) - if self.dir_in: - img_page_prediction = self.do_prediction(False, img, self.model_page) - else: - img_page_prediction = self.do_prediction(False, img, model_page) - - imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) - _, thresh = cv2.threshold(imgray, 0, 255, 0) - thresh = cv2.dilate(thresh, KERNEL, iterations=3) - contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) - cnt = contours[np.argmax(cnt_size)] - x, y, w, h = cv2.boundingRect(cnt) - box = [x, y, w, h] - else: - box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, img) - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - gc.collect() - self.logger.debug("exit early_page_for_num_of_column_classification") - return croped_page, page_coord - def extract_page(self): self.logger.debug("enter extract_page") cont_page = [] - if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(self.image, (5, 5), 0) - if not self.dir_in: - img_page_prediction = self.do_prediction(False, img, model_page) - else: - img_page_prediction = self.do_prediction(False, img, self.model_page) - imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) - _, thresh = cv2.threshold(imgray, 0, 255, 0) - thresh = cv2.dilate(thresh, KERNEL, iterations=3) - contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) - cnt = contours[np.argmax(cnt_size)] - x, y, w, h = cv2.boundingRect(cnt) - if x <= 30: - w += x - x = 0 - if (self.image.shape[1] - (x + w)) <= 30: - w = w + (self.image.shape[1] - (x + w)) - if y <= 30: - h = h + y - y = 0 - if (self.image.shape[0] - (y + h)) <= 30: - h = h + (self.image.shape[0] - (y + h)) + if not self.ignore_page_extraction: + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + img = cv2.GaussianBlur(self.image, (5, 5), 0) + if not self.dir_in: + img_page_prediction = self.do_prediction(False, img, model_page) + else: + img_page_prediction = self.do_prediction(False, img, self.model_page) + imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + thresh = cv2.dilate(thresh, KERNEL, iterations=3) + contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + if len(contours)>0: + cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt = contours[np.argmax(cnt_size)] + x, y, w, h = cv2.boundingRect(cnt) + if x <= 30: + w += x + x = 0 + if (self.image.shape[1] - (x + w)) <= 30: + w = w + (self.image.shape[1] - (x + w)) + if y <= 30: + h = h + y + y = 0 + if (self.image.shape[0] - (y + h)) <= 30: + h = h + (self.image.shape[0] - (y + h)) - box = [x, y, w, h] + box = [x, y, w, h] + else: + box = [0, 0, img.shape[1], img.shape[0]] + croped_page, page_coord = crop_image_inside_box(box, self.image) + cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() + gc.collect() + self.logger.debug("exit extract_page") else: - box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, self.image) - cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - gc.collect() - self.logger.debug("exit extract_page") + box = [0, 0, self.image.shape[1], self.image.shape[0]] + croped_page, page_coord = crop_image_inside_box(box, self.image) + cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) return croped_page, page_coord, cont_page def early_page_for_num_of_column_classification(self,img_bin): - self.logger.debug("enter early_page_for_num_of_column_classification") - if self.input_binary: - img =np.copy(img_bin) - img = img.astype(np.uint8) + if not self.ignore_page_extraction: + self.logger.debug("enter early_page_for_num_of_column_classification") + if self.input_binary: + img =np.copy(img_bin) + img = img.astype(np.uint8) + else: + img = self.imread() + if not self.dir_in: + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + img = cv2.GaussianBlur(img, (5, 5), 0) + + if self.dir_in: + img_page_prediction = self.do_prediction(False, img, self.model_page) + else: + img_page_prediction = self.do_prediction(False, img, model_page) + + imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(imgray, 0, 255, 0) + thresh = cv2.dilate(thresh, KERNEL, iterations=3) + contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + if len(contours)>0: + cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt = contours[np.argmax(cnt_size)] + x, y, w, h = cv2.boundingRect(cnt) + box = [x, y, w, h] + else: + box = [0, 0, img.shape[1], img.shape[0]] + croped_page, page_coord = crop_image_inside_box(box, img) + + if not self.dir_in: + session_page.close() + del model_page + del session_page + K.clear_session() + + gc.collect() + + self.logger.debug("exit early_page_for_num_of_column_classification") else: img = self.imread() - if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(img, (5, 5), 0) - - if self.dir_in: - img_page_prediction = self.do_prediction(False, img, self.model_page) - else: - img_page_prediction = self.do_prediction(False, img, model_page) - - imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) - _, thresh = cv2.threshold(imgray, 0, 255, 0) - thresh = cv2.dilate(thresh, KERNEL, iterations=3) - contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) - cnt = contours[np.argmax(cnt_size)] - x, y, w, h = cv2.boundingRect(cnt) - box = [x, y, w, h] - else: box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, img) - - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - - gc.collect() - - self.logger.debug("exit early_page_for_num_of_column_classification") + croped_page, page_coord = crop_image_inside_box(box, img) return croped_page, page_coord - def extract_page(self): - self.logger.debug("enter extract_page") - cont_page = [] - if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(self.image, (5, 5), 0) - if not self.dir_in: - img_page_prediction = self.do_prediction(False, img, model_page) - else: - img_page_prediction = self.do_prediction(False, img, self.model_page) - imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) - _, thresh = cv2.threshold(imgray, 0, 255, 0) - thresh = cv2.dilate(thresh, KERNEL, iterations=3) - contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) - cnt = contours[np.argmax(cnt_size)] - x, y, w, h = cv2.boundingRect(cnt) - if x <= 30: - w += x - x = 0 - if (self.image.shape[1] - (x + w)) <= 30: - w = w + (self.image.shape[1] - (x + w)) - if y <= 30: - h = h + y - y = 0 - if (self.image.shape[0] - (y + h)) <= 30: - h = h + (self.image.shape[0] - (y + h)) - - box = [x, y, w, h] - else: - box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, self.image) - cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - gc.collect() - self.logger.debug("exit extract_page") - return croped_page, page_coord, cont_page def extract_text_regions(self, img, patches, cols): self.logger.debug("enter extract_text_regions") @@ -2960,10 +2893,15 @@ class Eynollah: #self.logger.info('cont_page %s', cont_page) if not num_col: + print('buraya galir??') self.logger.info("No columns detected, outputting an empty PAGE-XML") pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) self.logger.info("Job done in %.1fs", time.time() - t1) - return pcgts + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts t1 = time.time() if not self.light_version: From 34a061782c0c5bcb193e3621933a4c3020b6718a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Tue, 3 May 2022 23:19:01 +0200 Subject: [PATCH 043/412] depend on tensorflow instead of tensorflow-gpu (#76) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6f2ea48..0180d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # ocrd includes opencv, numpy, shapely, click ocrd >= 2.23.3 scikit-learn >= 0.23.2 -tensorflow-gpu >= 2.4.0 +tensorflow >= 2.4.0 imutils >= 0.5.3 matplotlib setuptools >= 50 From cd9920eea76ab1bb5538fb50785a36d451ca1146 Mon Sep 17 00:00:00 2001 From: vahid Date: Wed, 4 May 2022 17:01:42 +0200 Subject: [PATCH 044/412] extracting page --- qurator/eynollah/cli.py | 16 ++++++++++++---- qurator/eynollah/eynollah.py | 4 ++++ qurator/eynollah/plot.py | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 18ea583..6828cc5 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -54,6 +54,12 @@ from qurator.eynollah.eynollah import Eynollah help="if a directory is given, all plots needed for documentation will be saved there", type=click.Path(exists=True, file_okay=False), ) +@click.option( + "--save_page", + "-sp", + help="if a directory is given, page crop of image will be saved there", + type=click.Path(exists=True, file_okay=False), +) @click.option( "--enable-plotting/--disable-plotting", "-ep/-noep", @@ -129,6 +135,7 @@ def main( save_layout, save_deskewed, save_all, + save_page, enable_plotting, allow_enhancement, curved_line, @@ -144,11 +151,11 @@ def main( if log_level: setOverrideLogLevel(log_level) initLogging() - if not enable_plotting and (save_layout or save_deskewed or save_all or save_images or allow_enhancement): - print("Error: You used one of -sl, -sd, -sa, -si or -ae but did not enable plotting with -ep") + if not enable_plotting and (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): + print("Error: You used one of -sl, -sd, -sa, -sp, -si or -ae but did not enable plotting with -ep") sys.exit(1) - elif enable_plotting and not (save_layout or save_deskewed or save_all or save_images or allow_enhancement): - print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa, -si or -ae") + elif enable_plotting and not (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): + print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa, -sp, -si or -ae") sys.exit(1) eynollah = Eynollah( image_filename=image, @@ -159,6 +166,7 @@ def main( dir_of_layout=save_layout, dir_of_deskewed=save_deskewed, dir_of_all=save_all, + dir_save_page=save_page, enable_plotting=enable_plotting, allow_enhancement=allow_enhancement, curved_line=curved_line, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8957248..e56009b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -96,6 +96,7 @@ class Eynollah: dir_of_layout=None, dir_of_deskewed=None, dir_of_all=None, + dir_save_page=None, enable_plotting=False, allow_enhancement=False, curved_line=False, @@ -121,6 +122,7 @@ class Eynollah: self.dir_out = dir_out self.dir_in = dir_in self.dir_of_all = dir_of_all + self.dir_save_page = dir_save_page self.dir_of_deskewed = dir_of_deskewed self.dir_of_deskewed = dir_of_deskewed self.dir_of_cropped_images=dir_of_cropped_images @@ -140,6 +142,7 @@ class Eynollah: self.plotter = None if not enable_plotting else EynollahPlotter( dir_out=self.dir_out, dir_of_all=dir_of_all, + dir_save_page=dir_save_page, dir_of_deskewed=dir_of_deskewed, dir_of_cropped_images=dir_of_cropped_images, dir_of_layout=dir_of_layout, @@ -219,6 +222,7 @@ class Eynollah: self.plotter = None if not self.enable_plotting else EynollahPlotter( dir_out=self.dir_out, dir_of_all=self.dir_of_all, + dir_save_page=self.dir_save_page, dir_of_deskewed=self.dir_of_deskewed, dir_of_cropped_images=self.dir_of_cropped_images, dir_of_layout=self.dir_of_layout, diff --git a/qurator/eynollah/plot.py b/qurator/eynollah/plot.py index ec4e290..b01fc04 100644 --- a/qurator/eynollah/plot.py +++ b/qurator/eynollah/plot.py @@ -19,6 +19,7 @@ class EynollahPlotter(): *, dir_out, dir_of_all, + dir_save_page, dir_of_deskewed, dir_of_layout, dir_of_cropped_images, @@ -29,6 +30,7 @@ class EynollahPlotter(): ): self.dir_out = dir_out self.dir_of_all = dir_of_all + self.dir_save_page = dir_save_page self.dir_of_layout = dir_of_layout self.dir_of_cropped_images = dir_of_cropped_images self.dir_of_deskewed = dir_of_deskewed @@ -127,6 +129,8 @@ class EynollahPlotter(): def save_page_image(self, image_page): if self.dir_of_all is not None: cv2.imwrite(os.path.join(self.dir_of_all, self.image_filename_stem + "_page.png"), image_page) + if self.dir_save_page is not None: + cv2.imwrite(os.path.join(self.dir_save_page, self.image_filename_stem + "_page.png"), image_page) def save_enhanced_image(self, img_res): cv2.imwrite(os.path.join(self.dir_out, self.image_filename_stem + "_enhanced.png"), img_res) From ae7c42488930f18c5d437f7c74ebec1b5a74f338 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 13 May 2022 11:44:45 +0200 Subject: [PATCH 045/412] Update eynollah.py --- qurator/eynollah/eynollah.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index e56009b..820cbd7 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2897,7 +2897,6 @@ class Eynollah: #self.logger.info('cont_page %s', cont_page) if not num_col: - print('buraya galir??') self.logger.info("No columns detected, outputting an empty PAGE-XML") pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) self.logger.info("Job done in %.1fs", time.time() - t1) From 00be99d29b735700ab5f51359d7e8f47e0cdee53 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 17 May 2022 12:01:25 +0200 Subject: [PATCH 046/412] add short section on supported Python, TF and CUDA versions --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cb5d60..766946a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ Alternatively, you can also use `make` with these targets: `make install` or -`make install-dev` for editable installation +`make install-dev` for editable installation + +The current version of Eynollah runs on Python `>=3.6` with Tensorflow `>=2.4`. + +In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. ### Models From 01bfc3914d859a5da1e9fe2b62c481ea1a440a63 Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 19 May 2022 12:27:01 +0200 Subject: [PATCH 047/412] extracting page as an option --- qurator/eynollah/eynollah.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 820cbd7..c125b1a 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2870,6 +2870,7 @@ class Eynollah: self.ls_imgs = [1] for img_name in self.ls_imgs: + print(img_name,'img_name') t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) From 402c5339aca18101d1e182f2f672c6ef6f4ec5dd Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 22 Jul 2022 15:32:35 +0200 Subject: [PATCH 048/412] issue #77 is resolved --- qurator/eynollah/utils/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index da14139..e9f872c 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -294,7 +294,7 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x #print(args_to_be_unified,'args_to_be_unified') - return reading_orther_type,x_start_returned, x_end_returned ,y_sep_returned,y_diff_returned,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother + return reading_orther_type,x_start_returned, x_end_returned ,y_sep_returned,y_diff_returned,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y def crop_image_inside_box(box, img_org_copy): image_box = img_org_copy[box[1] : box[1] + box[3], box[0] : box[0] + box[2]] return image_box, [box[1], box[1] + box[3], box[0], box[0] + box[2]] @@ -1771,7 +1771,7 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho peaks_neg_tot_tables.append(peaks_neg_tot) - reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) + reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) @@ -2240,9 +2240,18 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho ##y_lines_by_order.append(int(splitter_y_new[i])) ##x_start_by_order.append(0) - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_starting[0]) - x_ending.append(x_ending[0]) + #y_type_2.append(int(splitter_y_new[i])) + #x_starting.append(x_starting[0]) + #x_ending.append(x_ending[0]) + + if len(new_main_sep_y)>0: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(0) + x_ending.append(len(peaks_neg_tot)-1) + else: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(x_starting[0]) + x_ending.append(x_ending[0]) y_type_2=np.array(y_type_2) From 8d5079c909b662eda0b4acf5ae2908455f0ff939 Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 22 Jul 2022 15:43:19 +0200 Subject: [PATCH 049/412] issue #77 is resolved on main branch --- qurator/eynollah/utils/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 2533455..128c5d3 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -294,7 +294,7 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x #print(args_to_be_unified,'args_to_be_unified') - return reading_orther_type,x_start_returned, x_end_returned ,y_sep_returned,y_diff_returned,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother + return reading_orther_type,x_start_returned, x_end_returned ,y_sep_returned,y_diff_returned,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y def crop_image_inside_box(box, img_org_copy): image_box = img_org_copy[box[1] : box[1] + box[3], box[0] : box[0] + box[2]] return image_box, [box[1], box[1] + box[3], box[0], box[0] + box[2]] @@ -1695,7 +1695,7 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho peaks_neg_tot_tables.append(peaks_neg_tot) - reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) + reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) @@ -2164,9 +2164,18 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho ##y_lines_by_order.append(int(splitter_y_new[i])) ##x_start_by_order.append(0) - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_starting[0]) - x_ending.append(x_ending[0]) + #y_type_2.append(int(splitter_y_new[i])) + #x_starting.append(x_starting[0]) + #x_ending.append(x_ending[0]) + + if len(new_main_sep_y)>0: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(0) + x_ending.append(len(peaks_neg_tot)-1) + else: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(x_starting[0]) + x_ending.append(x_ending[0]) y_type_2=np.array(y_type_2) From dbf91876e1e352fb9be7d76dad91630d4738cf21 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Sep 2022 13:06:18 +0200 Subject: [PATCH 050/412] Adapt to new location of models --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 920f15b..13becb1 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,11 @@ help: # Download and extract models to $(PWD)/models_eynollah models: models_eynollah -models_eynollah: models_eynollah.tar.gz - tar xf models_eynollah.tar.gz +models_eynollah: models_eynollah_renamed.tar.gz + tar xf models_eynollah_renamed.tar.gz models_eynollah.tar.gz: - wget 'https://qurator-data.de/eynollah/models_eynollah.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' # Install with pip install: From 07fe0d827dc4b525741b2c38e1dd68170db66363 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Sep 2022 13:09:09 +0200 Subject: [PATCH 051/412] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 13becb1..1f5308e 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: # Download and extract models to $(PWD)/models_eynollah models: models_eynollah -models_eynollah: models_eynollah_renamed.tar.gz +models_eynollah: models_eynollah.tar.gz tar xf models_eynollah_renamed.tar.gz models_eynollah.tar.gz: From 583cdcee2cb20f9e8de38213112be07a5b2a4c15 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 13 Sep 2022 15:07:00 +0200 Subject: [PATCH 052/412] new (hybrid cnn+transformer) textline model which can accelerate to extract contour textlines faster --- qurator/eynollah/cli.py | 10 +++- qurator/eynollah/eynollah.py | 103 ++++++++++++++++++++++++++++++----- qurator/eynollah/writer.py | 15 ++--- requirements.txt | 12 ++-- 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 6828cc5..ddf986e 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -76,7 +76,13 @@ from qurator.eynollah.eynollah import Eynollah "--curved-line/--no-curvedline", "-cl/-nocl", is_flag=True, - help="if this parameter set to true, this tool will try to return contoure of textlines instead of rectabgle bounding box of textline. This should be taken into account that with this option the tool need more time to do process.", + help="if this parameter set to true, this tool will try to return contoure of textlines instead of rectangle bounding box of textline. This should be taken into account that with this option the tool need more time to do process.", +) +@click.option( + "--textline_light/--no-textline_light", + "-tll/-notll", + is_flag=True, + help="if this parameter set to true, this tool will try to return contoure of textlines instead of rectangle bounding box of textline with a faster method.", ) @click.option( "--full-layout/--no-full-layout", @@ -139,6 +145,7 @@ def main( enable_plotting, allow_enhancement, curved_line, + textline_light, full_layout, tables, input_binary, @@ -170,6 +177,7 @@ def main( enable_plotting=enable_plotting, allow_enhancement=allow_enhancement, curved_line=curved_line, + textline_light=textline_light, full_layout=full_layout, tables=tables, input_binary=input_binary, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c125b1a..8de793c 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -30,6 +30,7 @@ from scipy.signal import find_peaks import matplotlib.pyplot as plt from scipy.ndimage import gaussian_filter1d from keras.backend import set_session +from tensorflow.keras import layers from .utils.contour import ( filter_contours_area_of_image, @@ -83,6 +84,60 @@ DPI_THRESHOLD = 298 MAX_SLOPE = 999 KERNEL = np.ones((5, 5), np.uint8) +projection_dim = 64 +patch_size = 1 +num_patches =21*21#14*14#28*28#14*14#28*28 + + +class Patches(layers.Layer): + def __init__(self, **kwargs): + super(Patches, self).__init__() + self.patch_size = patch_size + + def call(self, images): + batch_size = tf.shape(images)[0] + patches = tf.image.extract_patches( + images=images, + sizes=[1, self.patch_size, self.patch_size, 1], + strides=[1, self.patch_size, self.patch_size, 1], + rates=[1, 1, 1, 1], + padding="VALID", + ) + patch_dims = patches.shape[-1] + patches = tf.reshape(patches, [batch_size, -1, patch_dims]) + return patches + def get_config(self): + + config = super().get_config().copy() + config.update({ + 'patch_size': self.patch_size, + }) + return config + + +class PatchEncoder(layers.Layer): + def __init__(self, **kwargs): + super(PatchEncoder, self).__init__() + self.num_patches = num_patches + self.projection = layers.Dense(units=projection_dim) + self.position_embedding = layers.Embedding( + input_dim=num_patches, output_dim=projection_dim + ) + + def call(self, patch): + positions = tf.range(start=0, limit=self.num_patches, delta=1) + encoded = self.projection(patch) + self.position_embedding(positions) + return encoded + def get_config(self): + + config = super().get_config().copy() + config.update({ + 'num_patches': self.num_patches, + 'projection': self.projection, + 'position_embedding': self.position_embedding, + }) + return config + class Eynollah: def __init__( self, @@ -100,6 +155,7 @@ class Eynollah: enable_plotting=False, allow_enhancement=False, curved_line=False, + textline_light=False, full_layout=False, tables=False, input_binary=False, @@ -130,6 +186,7 @@ class Eynollah: self.enable_plotting = enable_plotting self.allow_enhancement = allow_enhancement self.curved_line = curved_line + self.textline_light = textline_light self.full_layout = full_layout self.tables = tables self.input_binary = input_binary @@ -151,6 +208,7 @@ class Eynollah: dir_out=self.dir_out, image_filename=self.image_filename, curved_line=self.curved_line, + textline_light = self.textline_light, pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') self.dir_models = dir_models @@ -165,7 +223,10 @@ class Eynollah: self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425.h5" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425.h5" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314.h5" - self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" + if self.textline_light: + self.model_textline_dir = dir_models + "/model_17.h5" + else: + self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" self.model_tables = dir_models + "/eynollah-tables_20210319.h5" if dir_in and light_version: @@ -603,7 +664,10 @@ class Eynollah: gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) - model = load_model(model_dir, compile=False) + try: + model = load_model(model_dir, compile=False) + except: + model = load_model(model_dir , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model, session @@ -1368,12 +1432,17 @@ class Eynollah: # plt.imshow(mask_only_con_region) # plt.show() - all_text_region_raw = np.copy(textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]]) - mask_only_con_region = mask_only_con_region[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] - - - all_text_region_raw[mask_only_con_region == 0] = 0 - cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, [slope_deskew][0], contours_par_per_process[mv], boxes_text[mv]) + + if self.textline_light: + all_text_region_raw = np.copy(textline_mask_tot_ea) + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(all_text_region_raw) + cnt_clean_rot = filter_contours_area_of_image(all_text_region_raw, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + else: + all_text_region_raw = np.copy(textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]]) + mask_only_con_region = mask_only_con_region[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, [slope_deskew][0], contours_par_per_process[mv], boxes_text[mv]) textlines_rectangles_per_each_subprocess.append(cnt_clean_rot) index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) @@ -1481,8 +1550,10 @@ class Eynollah: if not self.dir_in: session_textline.close() - - return prediction_textline[:, :, 0], prediction_textline_longshot_true_size[:, :, 0] + if self.textline_light: + return (prediction_textline[:, :, 0]==1)*1, (prediction_textline_longshot_true_size[:, :, 0]==1)*1 + else: + return prediction_textline[:, :, 0], prediction_textline_longshot_true_size[:, :, 0] def do_work_of_slopes(self, q, poly, box_sub, boxes_per_process, textline_mask_tot, contours_per_process): self.logger.debug('enter do_work_of_slopes') @@ -2562,6 +2633,8 @@ class Eynollah: scaler_h_textline = 1 # 1.2#1.2 scaler_w_textline = 1 # 0.9#1 textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline) + if self.textline_light: + textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) if not self.dir_in: K.clear_session() if self.plotter: @@ -2870,7 +2943,6 @@ class Eynollah: self.ls_imgs = [1] for img_name in self.ls_imgs: - print(img_name,'img_name') t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) @@ -2887,6 +2959,7 @@ class Eynollah: num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) #self.logger.info("run graphics %.1fs ", time.time() - t1t) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) else: text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) self.logger.info("Textregion detection took %.1fs ", time.time() - t1) @@ -3043,8 +3116,12 @@ class Eynollah: if not self.curved_line: if self.light_version: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + if self.textline_light: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index d36d3ab..d5704f6 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -22,12 +22,13 @@ import numpy as np class EynollahXmlWriter(): - def __init__(self, *, dir_out, image_filename, curved_line, pcgts=None): + def __init__(self, *, dir_out, image_filename, curved_line,textline_light, pcgts=None): self.logger = getLogger('eynollah.writer') self.counter = EynollahIdCounter() self.dir_out = dir_out self.image_filename = image_filename self.curved_line = curved_line + self.textline_light = textline_light self.pcgts = pcgts self.scale_x = None # XXX set outside __init__ self.scale_y = None # XXX set outside __init__ @@ -60,7 +61,7 @@ class EynollahXmlWriter(): marginal_region.add_TextLine(textline) points_co = '' for l in range(len(all_found_texline_polygons_marginals[marginal_idx][j])): - if not self.curved_line: + if not (self.curved_line or self.textline_light): if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: textline_x_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x) ) textline_y_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y) ) @@ -70,7 +71,7 @@ class EynollahXmlWriter(): points_co += str(textline_x_coord) points_co += ',' points_co += str(textline_y_coord) - if self.curved_line and np.abs(slopes_marginals[marginal_idx]) <= 45: + if (self.curved_line or self.textline_light) and np.abs(slopes_marginals[marginal_idx]) <= 45: if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + page_coord[2]) / self.scale_x)) points_co += ',' @@ -80,7 +81,7 @@ class EynollahXmlWriter(): points_co += ',' points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][1] + page_coord[0]) / self.scale_y)) - elif self.curved_line and np.abs(slopes_marginals[marginal_idx]) > 45: + elif (self.curved_line or self.textline_light) and np.abs(slopes_marginals[marginal_idx]) > 45: if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x)) points_co += ',' @@ -101,7 +102,7 @@ class EynollahXmlWriter(): region_bboxes = all_box_coord[region_idx] points_co = '' for idx_contour_textline, contour_textline in enumerate(all_found_texline_polygons[region_idx][j]): - if not self.curved_line: + if not (self.curved_line or self.textline_light): if len(contour_textline) == 2: textline_x_coord = max(0, int((contour_textline[0] + region_bboxes[2] + page_coord[2]) / self.scale_x)) textline_y_coord = max(0, int((contour_textline[1] + region_bboxes[0] + page_coord[0]) / self.scale_y)) @@ -112,7 +113,7 @@ class EynollahXmlWriter(): points_co += ',' points_co += str(textline_y_coord) - if self.curved_line and np.abs(slopes[region_idx]) <= 45: + if (self.curved_line or self.textline_light) and np.abs(slopes[region_idx]) <= 45: if len(contour_textline) == 2: points_co += str(int((contour_textline[0] + page_coord[2]) / self.scale_x)) points_co += ',' @@ -121,7 +122,7 @@ class EynollahXmlWriter(): points_co += str(int((contour_textline[0][0] + page_coord[2]) / self.scale_x)) points_co += ',' points_co += str(int((contour_textline[0][1] + page_coord[0])/self.scale_y)) - elif self.curved_line and np.abs(slopes[region_idx]) > 45: + elif (self.curved_line or self.textline_light) and np.abs(slopes[region_idx]) > 45: if len(contour_textline)==2: points_co += str(int((contour_textline[0] + region_bboxes[2] + page_coord[2])/self.scale_x)) points_co += ',' diff --git a/requirements.txt b/requirements.txt index 8520780..54bb55e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 2.23.3 -keras >= 2.3.1, < 2.4 -scikit-learn >= 0.23.2 -tensorflow-gpu >= 1.15, < 2 -imutils >= 0.5.3 +ocrd +keras == 2.6.0 +scikit-learn +tensorflow-gpu == 2.6.0 +imutils matplotlib -setuptools >= 50 +setuptools From 38bf0d8740d33e8d4ffdba9122b93924783e2cdf Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 13 Sep 2022 16:08:08 +0200 Subject: [PATCH 053/412] solving issue by loading model by directory as input --- qurator/eynollah/eynollah.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8de793c..0034b5f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -293,6 +293,7 @@ class Eynollah: dir_out=self.dir_out, image_filename=self.image_filename, curved_line=self.curved_line, + textline_light = self.textline_light, pcgts=self.pcgts) def imread(self, grayscale=False, uint8=True): key = 'img' @@ -2926,8 +2927,11 @@ class Eynollah: return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables def our_load_model(self, model_file): - - model = load_model(model_file, compile=False) + + try: + model = load_model(model_file, compile=False) + except: + model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model From 000402f0dc880d7460b61345b217595c7e3e4083 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Sep 2022 16:40:44 +0200 Subject: [PATCH 054/412] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7438427..cdc724b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Alternatively, you can also use `make` with these targets: `make install-dev` for editable installation +The current version of Eynollah runs on Python >=3.6 with Tensorflow >=2.4. + +In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. + ### Models In order to run this tool you need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). From b75d8afb1d467a286d67c5e97a3d154b28850df0 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Sep 2022 17:19:19 +0200 Subject: [PATCH 055/412] Update README.md --- README.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cdc724b..196c188 100644 --- a/README.md +++ b/README.md @@ -33,28 +33,29 @@ In case you want to train your own model to use with Eynollah, have a look at [s The command-line interface can be called like this: ```sh -eynollah \ --i \ --o \ --m \ --fl \ --ae \ --as \ --cl \ --si \ --sd \ --sa \ --tab \ --ib \ --ho \ --sl \ --ep --light --di - +eynollah -i -o -m [OPTIONS] ``` -The tool performs better with RGB images than greyscale/binarized images. +Additionally, the following optional parameters can be used to further configure the processing: + +```sh +-fl: the tool will perform full layout analysis including detection of marginalia and drop capitals +-ae: the tool will resize and enhance the image. The rescaled and enhanced image is saved to the output directory +-as: the tool will check whether the document needs rescaling or not +-cl: the tool will extract contours of curved textlines instead of rectangle bounding boxes +-si : when a directory is given here, the tool will save image regions detected in documents to this directory +-sd : when a directory is given, deskewed image will be saved to this directory +-sa : when a directory is given, plots of layout detection are saved to this directory +-tab: the tool will try to detect tables +-ib: the tool will binarize the input image +-ho: the tool will ignore headers in reading order detection +-sl : when a directory is given, plots of layout detection are saved to this directory +-ep: the tool will save a plot. This should be used alongside with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options +-light: the tool will apply a faster method for main region detection and deskewing +-di : the tool will process all images in the directory in batch mode +``` + +The tool performs better with RGB images as input than with greyscale or binarized images. ## Documentation @@ -126,7 +127,9 @@ Some heuristic methods are also employed to further improve the model prediction
click to expand/collapse
- + +The tool makes use of a combination of several models. For model training, please see [Training](https://github.com/qurator-spk/eynollah/blob/eynollah_light/README.md#training). + #### Enhancement model: The image enhancement model is again an image-to-image model, trained on document images with low quality and GT of corresponding images with higher quality. For training the image enhancement model, a total of 1127 document images underwent 11 different downscaling processes and consequently 11 different qualities for each image were derived. The resulting images were cropped into patches of 672*672 pixels. Adam is used as an optimizer and the learning rate is 1e-4. Scaling is the only augmentation applied for training. The model is trained with a batch size of 2 and for 5 epochs. From ffc7f8290648cb87796bd87063dd173c649645eb Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:48:21 +0200 Subject: [PATCH 056/412] Update README.md --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 196c188..9374962 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Alternatively, you can also use `make` with these targets: `make install-dev` for editable installation -The current version of Eynollah runs on Python >=3.6 with Tensorflow >=2.4. +The current version of Eynollah runs on Python `>=3.6` with Tensorflow `>=2.4`. In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. @@ -36,23 +36,23 @@ The command-line interface can be called like this: eynollah -i -o -m [OPTIONS] ``` -Additionally, the following optional parameters can be used to further configure the processing: +The following options can be used to further configure the processing: ```sh --fl: the tool will perform full layout analysis including detection of marginalia and drop capitals --ae: the tool will resize and enhance the image. The rescaled and enhanced image is saved to the output directory --as: the tool will check whether the document needs rescaling or not --cl: the tool will extract contours of curved textlines instead of rectangle bounding boxes --si : when a directory is given here, the tool will save image regions detected in documents to this directory --sd : when a directory is given, deskewed image will be saved to this directory --sa : when a directory is given, plots of layout detection are saved to this directory --tab: the tool will try to detect tables --ib: the tool will binarize the input image --ho: the tool will ignore headers in reading order detection --sl : when a directory is given, plots of layout detection are saved to this directory --ep: the tool will save a plot. This should be used alongside with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options --light: the tool will apply a faster method for main region detection and deskewing --di : the tool will process all images in the directory in batch mode +-fl: perform full layout analysis including detection of marginalia and drop capitals +-ae: allow resizing and enhancing the input image, a rescaled and enhanced image is saved to the output directory +-as: allow scaling - check whether the input image needs rescaling or not +-cl: extract contours of curved textlines instead of rectangle bounding boxes +-si : save image regions detected in documents to this directory +-sd : save deskewed image to this directory +-sa : save plot of layout detection to this directory +-tab: try to detect tables +-ib: allow binarization of the input image +-ho: ignore headers in reading order detection +-sl : save plots of layout detection to this directory +-ep: save a plot. This should be used alongside with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options +-light: apply a faster but simpler method for main region detection and deskewing +-di : process all images in a directory in batch mode ``` The tool performs better with RGB images as input than with greyscale or binarized images. From 5ca857018b9e4f4cc0dd64c60d17254da091e6c7 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:26:36 +0200 Subject: [PATCH 057/412] Update README.md --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9374962..e307624 100644 --- a/README.md +++ b/README.md @@ -38,21 +38,21 @@ eynollah -i -o -m : save image regions detected in documents to this directory --sd : save deskewed image to this directory --sa : save plot of layout detection to this directory --tab: try to detect tables --ib: allow binarization of the input image --ho: ignore headers in reading order detection --sl : save plots of layout detection to this directory --ep: save a plot. This should be used alongside with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options --light: apply a faster but simpler method for main region detection and deskewing --di : process all images in a directory in batch mode +``` +-fl perform full layout analysis including detection of marginalia and drop capitals +-tab try to detect tables +-light apply a faster but simpler method for main region detection and deskewing +-ae allow resizing and enhancing the input image, the enhanced image is saved to the output directory +-as allow scaling - automatically check whether the input image needs scaling or not +-ib allow binarization of the input image +-ho ignore headers for reading order prediction +-cl extract contours of curved textlines instead of rectangle bounding boxes +-ep enables plotting. This MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options +-di process all images in a directory in batch mode +-si save image regions detected in documents to this directory +-sd save deskewed image to this directory +-sl save layout prediction as plot to this directory +-sa save all outputs (plot, enhanced or binary image and layout prediction) to this directory ``` The tool performs better with RGB images as input than with greyscale or binarized images. From 30ef006dfd1e525def5622575ca32f5b36123f52 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:28:54 +0200 Subject: [PATCH 058/412] Update README.md Clarify CLI options --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e307624..bcc667b 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ eynollah -i -o -m Date: Tue, 7 Feb 2023 13:36:16 +0300 Subject: [PATCH 059/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 766946a..82c93d7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Installation `pip install .` or -`pip install . -e` for editable installation +`pip install -e .` for editable installation Alternatively, you can also use `make` with these targets: From ac69136e8f9e5d8117279d3acfe4829b89ce69d2 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 9 Feb 2023 20:24:19 +0100 Subject: [PATCH 060/412] Update config.yml (#89) * Update config.yml * Update config.yml --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 72b2c5a..1a11d46 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,9 +2,9 @@ version: 2 jobs: - build-python36: + build-python37: docker: - - image: python:3.6 + - image: python:3.7 steps: - checkout - restore_cache: @@ -23,6 +23,6 @@ workflows: version: 2 build: jobs: - - build-python36 - #- build-python37 - #- build-python38 # no tensorflow for python 3.8 + #- build-python36 + - build-python37 + #- build-python38 From 79e897d3b2877d6002448b1e9d75e331636b078b Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 10 Feb 2023 00:56:52 +0000 Subject: [PATCH 061/412] try loading as TF SavedModel instead of HDF5 --- qurator/eynollah/eynollah.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ff3ceac..d6f70c3 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -515,6 +515,9 @@ class Eynollah: gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): + # prefer SavedModel over HDF5 format if it exists + model_dir = model_dir[:-3] model = load_model(model_dir, compile=False) return model, session From ab4bb7cd7b76d4d04d3dd2273b398153058e7cc4 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 11 Feb 2023 11:58:40 +0000 Subject: [PATCH 062/412] silentium! --- qurator/eynollah/eynollah.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index d6f70c3..22c45ad 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -220,7 +220,8 @@ class Eynollah: index_y_d = img_h - img_height_model img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model_enhancement.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2])) + label_p_pred = model_enhancement.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), + verbose=0) seg = label_p_pred[0, :, :, :] seg = seg * 255 @@ -355,7 +356,7 @@ class Eynollah: img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] - label_p_pred = model_num_classifier.predict(img_in) + label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) @@ -428,7 +429,7 @@ class Eynollah: - label_p_pred = model_num_classifier.predict(img_in) + label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) session_col_classifier.close() @@ -534,7 +535,8 @@ class Eynollah: img = img / float(255.0) img = resize_image(img, img_height_model, img_width_model) - label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2])) + label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), + verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) @@ -586,7 +588,8 @@ class Eynollah: index_y_d = img_h - img_height_model img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2])) + label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), + verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) From 2d9ccac35416afe24553147c429035c94cc2bf24 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 11 Feb 2023 12:04:16 +0000 Subject: [PATCH 063/412] contours: simplify --- qurator/eynollah/eynollah.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 22c45ad..301f750 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2359,12 +2359,12 @@ class Eynollah: contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) #self.logger.info('areas_cnt_text %s', areas_cnt_text) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) @@ -2376,14 +2376,14 @@ class Eynollah: contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - areas_cnt_text_d = np.array([cv2.contourArea(contours_only_text_parent_d[j]) for j in range(len(contours_only_text_parent_d))]) + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d=np.argsort(areas_cnt_text_d) - contours_only_text_parent_d=list(np.array(contours_only_text_parent_d)[index_con_parents_d] ) - areas_cnt_text_d=list(np.array(areas_cnt_text_d)[index_con_parents_d] ) + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d)[index_con_parents_d]) + areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) @@ -2438,12 +2438,12 @@ class Eynollah: contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) From 13bc2378d952f1ef7637480304d5383a45af789d Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:33:44 +0100 Subject: [PATCH 064/412] Update config.yml (#90) * Update config.yml enable CI for Python 3.8 * Update test-eynollah.yml Use 3.7 for actions --- .circleci/config.yml | 19 ++++++++++++++++++- .github/workflows/test-eynollah.yml | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1a11d46..23eb724 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,10 +19,27 @@ jobs: - run: make install - run: make smoke-test + build-python38: + docker: + - image: python:3.8 + steps: + - checkout + - restore_cache: + keys: + - model-cache + - run: make models + - save_cache: + key: model-cache + paths: + models_eynollah.tar.gz + models_eynollah + - run: make install + - run: make smoke-test + workflows: version: 2 build: jobs: #- build-python36 - build-python37 - #- build-python38 + - build-python38 diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 1afd2a6..9e8d7b1 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.6'] # '3.7' + python-version: ['3.7'] # '3.8' steps: - uses: actions/checkout@v2 From a56988a35a528aba7cefd85e1a83257f598a5085 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 11 Feb 2023 12:05:49 +0000 Subject: [PATCH 065/412] contours: numpy now needs dtype=object --- qurator/eynollah/eynollah.py | 10 +++++----- qurator/eynollah/utils/contour.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 301f750..c854b46 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2367,7 +2367,7 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + contours_only_text_parent = list(np.array(contours_only_text_parent, dtype=object)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -2382,7 +2382,7 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d)[index_con_parents_d]) + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d, dtype=object)[index_con_parents_d]) areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) @@ -2446,7 +2446,7 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + contours_only_text_parent = list(np.array(contours_only_text_parent, dtype=object)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -2473,7 +2473,7 @@ class Eynollah: K.clear_session() if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) else: contours_only_text_parent_d_ordered = None @@ -2566,7 +2566,7 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index 6b81391..d8a8af9 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -19,7 +19,7 @@ def contours_in_same_horizon(cy_main_hor): list_h.append(i) if len(list_h) > 1: all_args.append(list(set(list_h))) - return np.unique(all_args) + return np.unique(np.array(all_args, dtype=object)) def find_contours_mean_y_diff(contours_main): M_main = [cv2.moments(contours_main[j]) for j in range(len(contours_main))] From 7345f6bf678f36cf3a51576b0fa94df0919925d7 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 11 Feb 2023 17:11:34 +0000 Subject: [PATCH 066/412] remove TF1 session and GC controls, avoid repeating load_model --- qurator/eynollah/eynollah.py | 154 ++++++----------------------------- 1 file changed, 26 insertions(+), 128 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c854b46..170b5a7 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -145,6 +145,8 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/model_ensemble_s.h5" self.model_textline_dir = dir_models + "/model_textline_newspapers.h5" self.model_tables = dir_models + "/model_tables_ens_mixed_new_2.h5" + + self.models = {} def _cache_images(self, image_filename=None, image_pil=None): ret = {} @@ -255,11 +257,6 @@ class Eynollah: prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg prediction_true = prediction_true.astype(int) - session_enhancement.close() - del model_enhancement - del session_enhancement - gc.collect() - return prediction_true def calculate_width_height_by_columns(self, img, num_col, width_early, label_p_pred): @@ -361,16 +358,6 @@ class Eynollah: self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - session_col_classifier.close() - - del model_num_classifier - del session_col_classifier - - K.clear_session() - gc.collect() - - - img_new, _ = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) if img_new.shape[1] > img.shape[1]: @@ -394,11 +381,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - session_bin.close() - del model_bin - del session_bin - gc.collect() - prediction_bin = prediction_bin.astype(np.uint8) img= np.copy(prediction_bin) img_bin = np.copy(prediction_bin) @@ -428,12 +410,9 @@ class Eynollah: img_in[0, :, :, 2] = img_1ch[:, :] - label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 - self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - session_col_classifier.close() - K.clear_session() + self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) if dpi < DPI_THRESHOLD: img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) @@ -444,9 +423,6 @@ class Eynollah: image_res = np.copy(img) is_image_enhanced = False - session_col_classifier.close() - - self.logger.debug("exit resize_and_enhance_image_with_column_classifier") return is_image_enhanced, img, image_res, num_col, num_column_is_classified, img_bin @@ -513,15 +489,24 @@ class Eynollah: def start_new_session_and_model(self, model_dir): self.logger.debug("enter start_new_session_and_model (model_dir=%s)", model_dir) - gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) + #gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) - session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + physical_devices = tf.config.list_physical_devices('GPU') + try: + tf.config.experimental.set_memory_growth(physical_devices[0], True) + except: + self.logger.warning("no GPU device available") if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): # prefer SavedModel over HDF5 format if it exists model_dir = model_dir[:-3] - model = load_model(model_dir, compile=False) + if model_dir in self.models: + model = self.models[model_dir] + else: + model = load_model(model_dir, compile=False) + self.models[model_dir] = model - return model, session + return model, None def do_prediction(self, patches, img, model, marginal_of_patch_percent=0.1): self.logger.debug("enter do_prediction") @@ -640,8 +625,6 @@ class Eynollah: prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color prediction_true = prediction_true.astype(np.uint8) - del model - gc.collect() return prediction_true def early_page_for_num_of_column_classification(self,img_bin): @@ -668,19 +651,15 @@ class Eynollah: else: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, img) - session_page.close() - del model_page - del session_page - gc.collect() - K.clear_session() self.logger.debug("exit early_page_for_num_of_column_classification") return croped_page, page_coord def extract_page(self): self.logger.debug("enter extract_page") cont_page = [] - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(self.image, (5, 5), 0) + + model_page, session_page = self.start_new_session_and_model(self.model_page_dir) img_page_prediction = self.do_prediction(False, img, model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -707,11 +686,6 @@ class Eynollah: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, self.image) cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - session_page.close() - del model_page - del session_page - gc.collect() - K.clear_session() self.logger.debug("exit extract_page") return croped_page, page_coord, cont_page @@ -807,11 +781,6 @@ class Eynollah: prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) - session_region.close() - del model_region - del session_region - gc.collect() - self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 @@ -1112,9 +1081,6 @@ class Eynollah: prediction_textline_longshot = self.do_prediction(False, img, model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) - session_textline.close() - - return prediction_textline[:, :, 0], prediction_textline_longshot_true_size[:, :, 0] def do_work_of_slopes(self, q, poly, box_sub, boxes_per_process, textline_mask_tot, contours_per_process): @@ -1191,11 +1157,6 @@ class Eynollah: ##plt.show() prediction_regions_org=prediction_regions_org[:,:,0] prediction_regions_org[(prediction_regions_org[:,:]==1) & (mask_zeros_y[:,:]==1)]=0 - - session_region.close() - del model_region - del session_region - gc.collect() model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p2) img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1])) @@ -1203,11 +1164,6 @@ class Eynollah: prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) - session_region.close() - del model_region - del session_region - gc.collect() - mask_zeros2 = (prediction_regions_org2[:,:,0] == 0) mask_lines2 = (prediction_regions_org2[:,:,0] == 3) text_sume_early = (prediction_regions_org[:,:] == 1).sum() @@ -1247,12 +1203,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - session_bin.close() - del model_bin - del session_bin - gc.collect() - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 @@ -1266,11 +1216,6 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] mask_lines_only=(prediction_regions_org[:,:]==3)*1 - session_region.close() - del model_region - del session_region - gc.collect() - mask_texts_only=(prediction_regions_org[:,:]==1)*1 mask_images_only=(prediction_regions_org[:,:]==2)*1 @@ -1289,20 +1234,12 @@ class Eynollah: text_regions_p_true=cv2.fillPoly(text_regions_p_true,pts=polygons_of_only_texts, color=(1,1,1)) - - - K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml except: if self.input_binary: prediction_bin = np.copy(img_org) else: - session_region.close() - del model_region - del session_region - gc.collect() - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_org, model_bin) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) @@ -1314,15 +1251,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - - session_bin.close() - del model_bin - del session_bin - gc.collect() - - - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 ratio_x=1 @@ -1335,11 +1263,6 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] #mask_lines_only=(prediction_regions_org[:,:]==3)*1 - session_region.close() - del model_region - del session_region - gc.collect() - #img = resize_image(img_org, int(img_org.shape[0]*1), int(img_org.shape[1]*1)) #prediction_regions_org = self.do_prediction(True, img, model_region) @@ -1349,11 +1272,6 @@ class Eynollah: #prediction_regions_org = prediction_regions_org[:,:,0] #prediction_regions_org[(prediction_regions_org[:,:] == 1) & (mask_zeros_y[:,:] == 1)]=0 - #session_region.close() - #del model_region - #del session_region - #gc.collect() - @@ -1381,7 +1299,7 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) erosion_hurts = True - K.clear_session() + return text_regions_p_true, erosion_hurts, polygons_lines_xml def do_order_of_regions_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -1873,9 +1791,8 @@ class Eynollah: img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] - + prediction_ext = self.do_prediction(patches, img_new, model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) pre_updown = cv2.flip(pre_updown, -1) @@ -1896,9 +1813,8 @@ class Eynollah: img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] - + prediction_ext = self.do_prediction(patches, img_new, model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) pre_updown = cv2.flip(pre_updown, -1) @@ -1911,12 +1827,10 @@ class Eynollah: else: prediction_table = np.zeros(img.shape) img_w_half = int(img.shape[1]/2.) - + pre1 = self.do_prediction(patches, img[:,0:img_w_half,:], model_region) pre2 = self.do_prediction(patches, img[:,img_w_half:,:], model_region) - pre_full = self.do_prediction(patches, img[:,:,:], model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), model_region) pre_updown = cv2.flip(pre_updown, -1) @@ -1939,11 +1853,6 @@ class Eynollah: prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) - del model_region - del session_region - gc.collect() - - return prediction_table_erode.astype(np.int16) def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): @@ -1995,7 +1904,7 @@ class Eynollah: self.logger.info("Resizing and enhancing image...") is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier() self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') - K.clear_session() + scale = 1 if is_image_enhanced: if self.allow_enhancement: @@ -2019,7 +1928,7 @@ class Eynollah: scaler_h_textline = 1 # 1.2#1.2 scaler_w_textline = 1 # 0.9#1 textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline) - K.clear_session() + if self.plotter: self.plotter.save_plot_of_textlines(textline_mask_tot_ea, image_page) return textline_mask_tot_ea @@ -2032,7 +1941,7 @@ class Eynollah: if self.plotter: self.plotter.save_deskewed_image(slope_deskew) - self.logger.info("slope_deskew: %s", slope_deskew) + self.logger.info("slope_deskew: %.2f°", slope_deskew) return slope_deskew, slope_first def run_marginals(self, image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction): @@ -2081,7 +1990,6 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - K.clear_session() self.logger.info("num_col_classifier: %s", num_col_classifier) @@ -2147,7 +2055,6 @@ class Eynollah: contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) - K.clear_session() self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables @@ -2178,8 +2085,6 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) - K.clear_session() - gc.collect() if num_col_classifier>=3: if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -2246,21 +2151,18 @@ class Eynollah: text_regions_p[:, :][text_regions_p[:, :] == 3] = 6 text_regions_p[:, :][text_regions_p[:, :] == 4] = 8 - K.clear_session() image_page = image_page.astype(np.uint8) regions_fully, regions_fully_only_drop = self.extract_text_regions(image_page, True, cols=num_col_classifier) text_regions_p[:,:][regions_fully[:,:,0]==6]=6 regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 - K.clear_session() # plt.imshow(regions_fully[:,:,0]) # plt.show() regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully) # plt.imshow(regions_fully[:,:,0]) # plt.show() - K.clear_session() regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) # plt.imshow(regions_fully_np[:,:,0]) # plt.show() @@ -2271,7 +2173,6 @@ class Eynollah: # plt.imshow(regions_fully_np[:,:,0]) # plt.show() - K.clear_session() # plt.imshow(regions_fully[:,:,0]) # plt.show() regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) @@ -2297,7 +2198,6 @@ class Eynollah: if not self.tables: regions_without_separators = (text_regions_p[:, :] == 1) * 1 - K.clear_session() img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) self.logger.debug('exit run_boxes_full_layout') @@ -2470,7 +2370,7 @@ class Eynollah: all_found_texline_polygons = small_textlines_to_parent_adherence2(all_found_texline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_texline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_texline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - K.clear_session() + if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) @@ -2483,8 +2383,6 @@ class Eynollah: self.plotter.save_plot_of_layout(text_regions_p, image_page) self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - K.clear_session() - pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) all_found_texline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) From 5c26bdf402c8f82e185f9a3704e11d806a12546f Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 16 Feb 2023 13:56:47 +0000 Subject: [PATCH 067/412] ocrd-tool: add model archive to resmgr resources --- qurator/eynollah/ocrd-tool.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 220f2ea..9121868 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -44,7 +44,17 @@ "default": false, "description": "ignore the special role of headings during reading order detection" } - } + }, + "resources": [ + { + "description": "models for eynollah (TensorFlow format)", + "url": "https://ocr-d.kba.cloud/2021-04-25.SavedModel.tar.gz", + "name": "default", + "size": 1483106598, + "type": "archive", + "path_in_archive": "default" + } + ] } } } From 23f0c0b40a8005383d2fe72f18bf0016668f81ce Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 16 Feb 2023 18:50:57 +0000 Subject: [PATCH 068/412] ocrd-tool: replace by persistent model URL --- qurator/eynollah/ocrd-tool.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 9121868..ab97a4f 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -48,7 +48,7 @@ "resources": [ { "description": "models for eynollah (TensorFlow format)", - "url": "https://ocr-d.kba.cloud/2021-04-25.SavedModel.tar.gz", + "url": "https://qurator-data.de/eynollah/2021-04-25/SavedModel.tar.gz", "name": "default", "size": 1483106598, "type": "archive", From 318ea6acca1e4a76bdd0a26c554fd57f6db0ffa1 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 16 Feb 2023 14:40:02 +0000 Subject: [PATCH 069/412] OCR-D wrapper: expose tables param --- qurator/eynollah/ocrd-tool.json | 5 +++++ qurator/eynollah/processor.py | 1 + 2 files changed, 6 insertions(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index ab97a4f..b9b4020 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -29,6 +29,11 @@ "default": true, "description": "Try to detect all element subtypes, including drop-caps and headings" }, + "tables": { + "type": "boolean", + "default": false, + "description": "Try to detect table regions" + }, "curved_line": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 41b12ae..ccec456 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -50,6 +50,7 @@ class EynollahProcessor(Processor): 'full_layout': self.parameter['full_layout'], 'allow_scaling': self.parameter['allow_scaling'], 'headers_off': self.parameter['headers_off'], + 'tables': self.parameter['tables'], 'override_dpi': self.parameter['dpi'], 'logger': LOG, 'pcgts': pcgts, From 875e4fe32bbe1d87bbfcf776ae2f3b32c9b61f6f Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 16 Feb 2023 14:45:37 +0000 Subject: [PATCH 070/412] log number of detected regions --- qurator/eynollah/eynollah.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 170b5a7..264bb62 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -388,6 +388,7 @@ class Eynollah: img = self.imread() img_bin = None + t1 = time.time() _, page_coord = self.early_page_for_num_of_column_classification(img_bin) model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) @@ -413,6 +414,7 @@ class Eynollah: label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) + self.logger.info("detecting columns took %.1fs", time.time() - t1) if dpi < DPI_THRESHOLD: img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) @@ -2356,6 +2358,14 @@ class Eynollah: # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) else: pass + + self.logger.info("Found %d text regions", len(contours_only_text_parent)) + self.logger.info("Found %d margin regions", len(polygons_of_marginals)) + self.logger.info("Found %d image regions", len(polygons_of_images)) + self.logger.info("Found %d separator lines", len(polygons_lines_xml)) + if self.tables: + self.logger.info("Found %d tables", len(contours_tables)) + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) From 31a2ec8fe68bfbc1ec0515694ec834f205e1d415 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Wed, 22 Mar 2023 14:18:57 +0100 Subject: [PATCH 071/412] :memo: changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8815d6..3446404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +Fixed: + + * Do not produce spurious `TextEquiv`, #68 + * Less spammy logging, #64, #65, #71 + +Changed: + + * Upgrade to tensorflow 2.4.0, #74 + * Improved README + * CI: test for python 3.7+, #90 + ## [0.0.11] - 2022-02-02 Fixed: From 71d0ec8dfeed71fe8f18b684f231f1af52ed56e4 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Wed, 22 Mar 2023 14:21:53 +0100 Subject: [PATCH 072/412] :package: v0.1.0 --- CHANGELOG.md | 3 +++ qurator/eynollah/ocrd-tool.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3446404..240753f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.1.0] - 2023-03-22 + Fixed: * Do not produce spurious `TextEquiv`, #68 @@ -83,6 +85,7 @@ Fixed: Initial release +[0.1.0]: ../../compare/v0.1.0...v0.0.11 [0.0.11]: ../../compare/v0.0.11...v0.0.10 [0.0.10]: ../../compare/v0.0.10...v0.0.9 [0.0.9]: ../../compare/v0.0.9...v0.0.8 diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 220f2ea..03bc52a 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.0.11", + "version": "0.1.0", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From 7cd07dd550f1c1f4884aa2cb45339c44db878037 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Wed, 22 Mar 2023 16:19:00 +0100 Subject: [PATCH 073/412] use PEP420 style qurator namespace --- qurator/__init__.py | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/qurator/__init__.py b/qurator/__init__.py index 5284146..e69de29 100644 --- a/qurator/__init__.py +++ b/qurator/__init__.py @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/setup.py b/setup.py index 9abf158..c78ee3f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ setup( author='Vahid Rezanezhad', url='https://github.com/qurator-spk/eynollah', license='Apache License 2.0', - namespace_packages=['qurator'], packages=find_packages(exclude=['tests']), install_requires=install_requires, package_data={ From e167e0863d64928c9cad30375d6c5b9bad476fa2 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Fri, 24 Mar 2023 14:16:10 +0100 Subject: [PATCH 074/412] :memo: changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 240753f..eb44b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +Changed: + + * Convert default model from HDFS to TF SavedModel, #91 + +Added: + + * parmeter `tables` to toggle table detectino, #91 + * default model described in ocrd-tool.json, #91 + ## [0.1.0] - 2023-03-22 Fixed: From ea792d1e4ac4a722770b82dc91e71f84d5beb212 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Fri, 24 Mar 2023 14:16:55 +0100 Subject: [PATCH 075/412] :package: v0.2.0 --- CHANGELOG.md | 3 +++ qurator/eynollah/ocrd-tool.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb44b0c..9f6ceff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.2.0] - 2023-03-24 + Changed: * Convert default model from HDFS to TF SavedModel, #91 @@ -94,6 +96,7 @@ Fixed: Initial release +[0.2.0]: ../../compare/v0.2.0...v0.1.0 [0.1.0]: ../../compare/v0.1.0...v0.0.11 [0.0.11]: ../../compare/v0.0.11...v0.0.10 [0.0.10]: ../../compare/v0.0.10...v0.0.9 diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index e6a06e5..fc9ee72 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.2.0", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From 4807be1b6228dd18c35566f93c16d2480e05a2d2 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:14:29 +0200 Subject: [PATCH 076/412] Update requirements.txt --- requirements.txt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 54bb55e..0180d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ # ocrd includes opencv, numpy, shapely, click -ocrd -keras == 2.6.0 -scikit-learn -tensorflow-gpu == 2.6.0 -imutils +ocrd >= 2.23.3 +scikit-learn >= 0.23.2 +tensorflow >= 2.4.0 +imutils >= 0.5.3 matplotlib -setuptools +setuptools >= 50 From 4276417938e0fb9d83be1fd8cf192bd85b703c8a Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:32:42 +0200 Subject: [PATCH 077/412] Update README.md --- README.md | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bcc667b..1cadf5d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Installation `pip install .` or -`pip install . -e` for editable installation +`pip install -e .` for editable installation Alternatively, you can also use `make` with these targets: @@ -123,40 +123,12 @@ Some heuristic methods are also employed to further improve the model prediction
-### Model description - -
- click to expand/collapse
- -The tool makes use of a combination of several models. For model training, please see [Training](https://github.com/qurator-spk/eynollah/blob/eynollah_light/README.md#training). - -#### Enhancement model: -The image enhancement model is again an image-to-image model, trained on document images with low quality and GT of corresponding images with higher quality. For training the image enhancement model, a total of 1127 document images underwent 11 different downscaling processes and consequently 11 different qualities for each image were derived. The resulting images were cropped into patches of 672*672 pixels. Adam is used as an optimizer and the learning rate is 1e-4. Scaling is the only augmentation applied for training. The model is trained with a batch size of 2 and for 5 epochs. - -#### Classifier model: -In order to obtain high quality results, it is beneficial to scale the document image to the same scale of the images in the training dataset that the models were trained on. The classifier model predicts the number of columns in a document by creating a training set for that purpose with manual classification of all documents into six classes with either one, two, three, four, five, or six and more columns respectively. Classifier model is a ResNet50+2 dense layers on top. The input size of model is 448*448 and Adam is used as an optimizer and the learning rate is 1e-4. Model is trained for 300 epochs. - -#### Page extractor model: -This a deep learning model which helps to crop the page borders by using a pixel-wise segmentation method. In case of page extraction it is necessary to train the model on the entire (document) image, i.e. full images are resized to the input size of the model (no patches). For training, the model is fed with entire images from the 2820 samples of the extended training set. The input size of the the page extraction model is 448*448 pixels. Adam is used as an optimizer and the learning rate is 1e-6. The model is trained with a batch size of 4 and for 30 epochs. - -#### Early layout model: -The early layout detection model detects only the main and recursive regions in a document like background, text regions, separators and images. In the case of early layout segmentation, we used 381 pages to train the model. The model is fed with patches of size 448*672 pixels. Adam is used as an optimizer and the learning rate is 1e-4. Two models were trained, one with scale augmentation and another one without any augmentation. Both models were trained for 12 epochs and with a batch size of 3. Categorical cross entropy is used as a loss function. - -#### Full layout model: -By full layout detection we have added two more elements of a document structure, drop capitals and headings, onto early layout elements. For the secondary layout segmentation we have trained two models. One is trained with 355 pages containing 3 or more columns and in patches with a size of 896*896 pixels. The other model is trained on 634 pages that have only one column. The second model is fed with the entire image with input size -of 896 * 896 pixels (not in patches). Adam is used as an optimizer and the learning rate is 1e-4. Then both models are trained for 8 epochs with a batch size of 1. Soft dice is used as the loss function. - -#### Text line segmentation model: -For text line segmentation, 342 pages were used for training. The model is trained in patches with the size of 448*672. Adam is used as an optimizer and the learning rate is 1e-4. The training set is augmented with scaling and rotation. The model is trained only for 1 epoch with a batch size of 3. Soft dice is again used as the loss function. - -
- ### How to use
click to expand/collapse
-First, this model makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. +Eynollah makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. * If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. @@ -201,7 +173,7 @@ would still use the original (RGB) image despite any binarization that may have #### Eynollah "light" - Eynollah light has used a faster method to predict and extract early layout. On other hand with light version deskewing is not applied for any text region and in return it is done for the whole document once. The other option that users have with light version is that instead of image name a folder of images can be given as input and in this case all models will be loaded and then processing for all images will be implemented. This step accelerates process of document analysis. + Eynollah light uses a faster method to predict and extract the early layout. But with the light option enabled deskewing is not applied for any text region and done only once for the whole document.
From f37d324812bc90315e1a7b1002144bf877c61f82 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:36:50 +0200 Subject: [PATCH 078/412] Use renamed models in SavedModel format --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1f5308e..c9e7a11 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,10 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - tar xf models_eynollah_renamed.tar.gz + tar xf 2022-04-05.SavedModel.tar.gz models_eynollah.tar.gz: - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' + wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # Install with pip install: From 27834ce33de6eef2b81ef458dc3fd7fb924271dd Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:45:38 +0200 Subject: [PATCH 079/412] update CI --- .circleci/config.yml | 27 ++++++++++++++++++++++----- .github/workflows/test-eynollah.yml | 4 ++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 72b2c5a..8cf026c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,9 +2,26 @@ version: 2 jobs: - build-python36: + build-python37: docker: - - image: python:3.6 + - image: python:3.7 + steps: + - checkout + - restore_cache: + keys: + - model-cache + - run: make models + - save_cache: + key: model-cache + paths: + models_eynollah.tar.gz + models_eynollah + - run: make install + - run: make smoke-test + + build-python38: + docker: + - image: python:3.8 steps: - checkout - restore_cache: @@ -23,6 +40,6 @@ workflows: version: 2 build: jobs: - - build-python36 - #- build-python37 - #- build-python38 # no tensorflow for python 3.8 + #- build-python36 + - build-python37 + - build-python38 \ No newline at end of file diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 1afd2a6..de742f1 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.6'] # '3.7' + python-version: ['3.7'] # '3.8' steps: - uses: actions/checkout@v2 @@ -33,4 +33,4 @@ jobs: pip install . pip install -r requirements-test.txt - name: Test with pytest - run: make test + run: make test \ No newline at end of file From 4642ccb36d435ac0d13792d3127b7301ddcec5b9 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:54:50 +0200 Subject: [PATCH 080/412] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8cf026c..23eb724 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,4 +42,4 @@ workflows: jobs: #- build-python36 - build-python37 - - build-python38 \ No newline at end of file + - build-python38 From 2c13f1bddc8a9495e8a1ca138f7c62b6651d7292 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 29 Mar 2023 00:02:16 +0200 Subject: [PATCH 081/412] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cadf5d..02bbcb7 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Alternatively, you can also use `make` with these targets: `make install` or -`make install-dev` for editable installation +`make install-dev` for editable installation -The current version of Eynollah runs on Python `>=3.6` with Tensorflow `>=2.4`. +The current version of Eynollah runs on Python `>=3.6` with Tensorflow `>=2.4`. In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. From d21cc42d875930c779aaa96126e123732712ce6e Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 29 Mar 2023 00:21:02 +0200 Subject: [PATCH 082/412] Update README.md remove Python 3.6 from supported versions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02bbcb7..da11b82 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Alternatively, you can also use `make` with these targets: `make install-dev` for editable installation -The current version of Eynollah runs on Python `>=3.6` with Tensorflow `>=2.4`. +The current version of Eynollah runs on Python `>=3.7` with Tensorflow `>=2.4`. In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. From 58ca226f2db847e3a837afc8aee28f8a217c82c1 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 29 Mar 2023 00:54:51 +0200 Subject: [PATCH 083/412] apply some fixes from main --- qurator/eynollah/eynollah.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 0034b5f..be490fe 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -20,10 +20,10 @@ import numpy as np os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" stderr = sys.stderr sys.stderr = open(os.devnull, "w") -from keras import backend as K -from keras.models import load_model -sys.stderr = stderr import tensorflow as tf +from tensorflow.python.keras import backend as K +from tensorflow.keras.models load_model +sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") from scipy.signal import find_peaks @@ -699,7 +699,7 @@ class Eynollah: if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) - self.logger.info("Image dimensions: %sx%s", img_height_model, img_width_model) + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin From 3d54719c87152ae94e4cc2c5572bc554825fae57 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 29 Mar 2023 01:01:19 +0200 Subject: [PATCH 084/412] fix import --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index be490fe..6500c2e 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -22,7 +22,7 @@ stderr = sys.stderr sys.stderr = open(os.devnull, "w") import tensorflow as tf from tensorflow.python.keras import backend as K -from tensorflow.keras.models load_model +from tensorflow.keras.models import load_model sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") From a078a18530b941c72b8e0c3695cdeabdf679a347 Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 22 Jul 2022 15:43:19 +0200 Subject: [PATCH 085/412] issue #77 is resolved on main branch From 73057d57d1fcfc3fab6495bf46570365f7988ca4 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 11 Feb 2023 11:58:40 +0000 Subject: [PATCH 086/412] silentium! --- qurator/eynollah/eynollah.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 6500c2e..6f776ae 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -356,7 +356,8 @@ class Eynollah: index_y_d = img_h - img_height_model img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model_enhancement.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2])) + label_p_pred = model_enhancement.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), + verbose=0) seg = label_p_pred[0, :, :, :] seg = seg * 255 @@ -491,10 +492,11 @@ class Eynollah: img_in[0, :, :, 0] = img_1ch[:, :] img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] + if not self.dir_in: - label_p_pred = model_num_classifier.predict(img_in) + label_p_pred = model_num_classifier.predict(img_in, verbose=0) else: - label_p_pred = self.model_classifier.predict(img_in) + label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) @@ -572,10 +574,11 @@ class Eynollah: if self.dir_in: - label_p_pred = self.model_classifier.predict(img_in) + label_p_pred = self.model_classifier.predict(img_in, verbose=0) else: - label_p_pred = model_num_classifier.predict(img_in) + label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 + self.logger.info("Found %s columns (%s)", num_col, label_p_pred) if not self.dir_in: session_col_classifier.close() @@ -684,7 +687,8 @@ class Eynollah: img = img / float(255.0) img = resize_image(img, img_height_model, img_width_model) - label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2])) + label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), + verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) @@ -736,7 +740,8 @@ class Eynollah: index_y_d = img_h - img_height_model img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2])) + label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), + verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) From 1ac0a7e06f968db8adb0e30e2cfebe5e8e8ce7c5 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 10 Feb 2023 00:56:52 +0000 Subject: [PATCH 087/412] try loading as TF SavedModel instead of HDF5 --- qurator/eynollah/eynollah.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 6f776ae..406964a 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -668,10 +668,15 @@ class Eynollah: gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) - try: - model = load_model(model_dir, compile=False) - except: - model = load_model(model_dir , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) + + # try: + # model = load_model(model_dir, compile=False) + # except: + # model = load_model(model_dir , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) + if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): + # prefer SavedModel over HDF5 format if it exists + model_dir = model_dir[:-3] + model = load_model(model_dir, compile=False) return model, session From 9849541061f417a859e03831dedee3f087d0b9d1 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 30 Mar 2023 22:22:36 +0200 Subject: [PATCH 088/412] Update Makefile test hdf5 models --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c9e7a11..90b9891 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,12 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - tar xf 2022-04-05.SavedModel.tar.gz + tar xf tar xf models_eynollah_renamed.tar.gz + # tar xf 2022-04-05.SavedModel.tar.gz models_eynollah.tar.gz: - wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' + # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # Install with pip install: From d4dd532212ce44c26421e79a6ff079ce4c71a5b2 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 30 Mar 2023 22:37:15 +0200 Subject: [PATCH 089/412] Update Makefile caj --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 90b9891..e5227b2 100644 --- a/Makefile +++ b/Makefile @@ -22,11 +22,13 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - tar xf tar xf models_eynollah_renamed.tar.gz + tar xf models_eynollah.tar.gz + # tar xf models_eynollah_renamed.tar.gz # tar xf 2022-04-05.SavedModel.tar.gz models_eynollah.tar.gz: - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' + wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' + # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # Install with pip From fb6d97091bc498502a841c4f445365401d93918e Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 16 Feb 2023 14:40:02 +0000 Subject: [PATCH 090/412] OCR-D wrapper: expose tables param --- qurator/eynollah/ocrd-tool.json | 5 +++++ qurator/eynollah/processor.py | 1 + 2 files changed, 6 insertions(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 220f2ea..1291979 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -29,6 +29,11 @@ "default": true, "description": "Try to detect all element subtypes, including drop-caps and headings" }, + "tables": { + "type": "boolean", + "default": false, + "description": "Try to detect table regions" + }, "curved_line": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 41b12ae..ccec456 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -50,6 +50,7 @@ class EynollahProcessor(Processor): 'full_layout': self.parameter['full_layout'], 'allow_scaling': self.parameter['allow_scaling'], 'headers_off': self.parameter['headers_off'], + 'tables': self.parameter['tables'], 'override_dpi': self.parameter['dpi'], 'logger': LOG, 'pcgts': pcgts, From a9728bb899315ae2672b1a0be3477dfce5fb1a10 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 30 Mar 2023 23:44:05 +0200 Subject: [PATCH 091/412] Update eynollah.py predict quietly please --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 406964a..f210fcd 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -827,7 +827,7 @@ class Eynollah: if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) - self.logger.info("Image dimensions: %sx%s", img_height_model, img_width_model) + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin From 817e5a6af920ec10c9816052df08148b6f8603f8 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 31 Mar 2023 01:32:10 +0200 Subject: [PATCH 092/412] update docstring --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index f210fcd..2bb09a1 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3,7 +3,7 @@ # pylint: disable=too-many-public-methods,too-many-arguments,too-many-instance-attributes,too-many-public-methods, # pylint: disable=consider-using-enumerate """ -tool to extract table form data from alto xml data +document layout analysis (segmentation) with output in PAGE-XML """ import math From 31be7892a069d582c3bf20f2f9eafd0e918cf89b Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 31 Mar 2023 02:19:07 +0200 Subject: [PATCH 093/412] Makefile hack to rename model dir --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e5227b2..b85a526 100644 --- a/Makefile +++ b/Makefile @@ -21,14 +21,14 @@ help: # Download and extract models to $(PWD)/models_eynollah models: models_eynollah -models_eynollah: models_eynollah.tar.gz - tar xf models_eynollah.tar.gz +models_eynollah: models_eynollah_renamed.tar.gz + tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz # tar xf 2022-04-05.SavedModel.tar.gz models_eynollah.tar.gz: - wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' - # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' + # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # Install with pip From fd4c0ed4e86e12a6834c0e49811be93b3040e599 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 31 Mar 2023 02:21:36 +0200 Subject: [PATCH 094/412] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b85a526..8706995 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: # Download and extract models to $(PWD)/models_eynollah models: models_eynollah -models_eynollah: models_eynollah_renamed.tar.gz +models_eynollah: models_eynollah.tar.gz tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz # tar xf 2022-04-05.SavedModel.tar.gz From aecc2ea543225578dc8eec85e26875a0903cdc44 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 31 Mar 2023 03:18:18 +0200 Subject: [PATCH 095/412] Update README.md added some badges --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index da11b82..29ed56f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Eynollah +[![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=svg)](https://circleci.com/gh/qurator-spk/eynollah) +[![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) +[![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://github.com/qurator-spk/eynollah/blob/main/LICENSE) > Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) From 0279ebfe1322756b73a00164b53b4258d59c0297 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 31 Mar 2023 03:19:44 +0200 Subject: [PATCH 096/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29ed56f..0d84993 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Eynollah [![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=svg)](https://circleci.com/gh/qurator-spk/eynollah) [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) -[![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://github.com/qurator-spk/eynollah/blob/main/LICENSE) +[![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://opensource.org/license/apache-2-0/) > Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) From 14fc04042841556f5e59f9e3ca185e8c1a004d1d Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Sun, 2 Apr 2023 14:07:51 +0200 Subject: [PATCH 097/412] use find_namespace_packages in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c78ee3f..807eae7 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_namespace_packages, find_packages, setup from json import load install_requires = open('requirements.txt').read().split('\n') From 22a8e93031b806044d0bcac1d30195b328417f27 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 5 Apr 2023 10:40:18 +0200 Subject: [PATCH 098/412] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d84993..e400d51 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Eynollah +> Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) + [![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=svg)](https://circleci.com/gh/qurator-spk/eynollah) [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) [![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://opensource.org/license/apache-2-0/) -> Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML). ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) From d3735b12f49b4f5b71ba576c392cd9ffcc74b408 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 11 Apr 2023 13:12:20 +0200 Subject: [PATCH 099/412] pushing commits 2d9ccac and 7345f6b into eynollah_light --- qurator/eynollah/eynollah.py | 204 +++++++---------------------------- 1 file changed, 38 insertions(+), 166 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2bb09a1..c9e6674 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -229,6 +229,8 @@ class Eynollah: self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" self.model_tables = dir_models + "/eynollah-tables_20210319.h5" + self.models = {} + if dir_in and light_version: config = tf.compat.v1.ConfigProto() config.gpu_options.allow_growth = True @@ -391,10 +393,6 @@ class Eynollah: prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg prediction_true = prediction_true.astype(int) - session_enhancement.close() - del model_enhancement - del session_enhancement - gc.collect() return prediction_true @@ -500,13 +498,6 @@ class Eynollah: num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - if not self.dir_in: - session_col_classifier.close() - - del model_num_classifier - del session_col_classifier - K.clear_session() - gc.collect() @@ -537,12 +528,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - if not self.dir_in: - session_bin.close() - del model_bin - del session_bin - gc.collect() - prediction_bin = prediction_bin.astype(np.uint8) img= np.copy(prediction_bin) img_bin = np.copy(prediction_bin) @@ -579,10 +564,7 @@ class Eynollah: label_p_pred = model_num_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 - self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - if not self.dir_in: - session_col_classifier.close() - K.clear_session() + self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) if dpi < DPI_THRESHOLD: img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) @@ -595,8 +577,6 @@ class Eynollah: num_column_is_classified = True image_res = np.copy(img) is_image_enhanced = False - if not self.dir_in: - session_col_classifier.close() self.logger.debug("exit resize_and_enhance_image_with_column_classifier") @@ -665,9 +645,14 @@ class Eynollah: def start_new_session_and_model(self, model_dir): self.logger.debug("enter start_new_session_and_model (model_dir=%s)", model_dir) - gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) + #gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) - session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + physical_devices = tf.config.list_physical_devices('GPU') + try: + tf.config.experimental.set_memory_growth(physical_devices[0], True) + except: + self.logger.warning("no GPU device available") # try: # model = load_model(model_dir, compile=False) @@ -676,9 +661,13 @@ class Eynollah: if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): # prefer SavedModel over HDF5 format if it exists model_dir = model_dir[:-3] - model = load_model(model_dir, compile=False) + if model_dir in self.models: + model = self.models[model_dir] + else: + model = load_model(model_dir, compile=False) + self.models[model_dir] = model - return model, session + return model, None def do_prediction(self, patches, img, model, marginal_of_patch_percent=0.1): self.logger.debug("enter do_prediction") @@ -797,8 +786,8 @@ class Eynollah: prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color prediction_true = prediction_true.astype(np.uint8) - del model - gc.collect() + #del model + #gc.collect() return prediction_true def do_prediction_new_concept(self, patches, img, model, marginal_of_patch_percent=0.1): self.logger.debug("enter do_prediction") @@ -963,17 +952,19 @@ class Eynollah: prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color prediction_true = prediction_true.astype(np.uint8) - del model - gc.collect() + ##del model + ##gc.collect() return prediction_true def extract_page(self): self.logger.debug("enter extract_page") cont_page = [] if not self.ignore_page_extraction: + img = cv2.GaussianBlur(self.image, (5, 5), 0) + if not self.dir_in: model_page, session_page = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(self.image, (5, 5), 0) + if not self.dir_in: img_page_prediction = self.do_prediction(False, img, model_page) else: @@ -1003,12 +994,7 @@ class Eynollah: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, self.image) cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - gc.collect() + self.logger.debug("exit extract_page") else: box = [0, 0, self.image.shape[1], self.image.shape[0]] @@ -1046,14 +1032,6 @@ class Eynollah: box = [0, 0, img.shape[1], img.shape[0]] croped_page, page_coord = crop_image_inside_box(box, img) - if not self.dir_in: - session_page.close() - del model_page - del session_page - K.clear_session() - - gc.collect() - self.logger.debug("exit early_page_for_num_of_column_classification") else: img = self.imread() @@ -1156,12 +1134,6 @@ class Eynollah: prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() - self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 @@ -1558,8 +1530,6 @@ class Eynollah: prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) - if not self.dir_in: - session_textline.close() if self.textline_light: return (prediction_textline[:, :, 0]==1)*1, (prediction_textline_longshot_true_size[:, :, 0]==1)*1 @@ -1631,8 +1601,6 @@ class Eynollah: else: img_w_new = 4000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) - gc.collect() - ##img_resized = resize_image(img_bin,img_height_h, img_width_h ) img_resized = resize_image(img,img_h_new, img_w_new ) if not self.dir_in: @@ -1645,11 +1613,6 @@ class Eynollah: prediction_bin = prediction_bin*255 prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - if not self.dir_in: - session_bin.close() - del model_bin - del session_bin - gc.collect() prediction_bin = prediction_bin.astype(np.uint16) #img= np.copy(prediction_bin) @@ -1695,9 +1658,6 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - #erosion_hurts = True - if not self.dir_in: - K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): @@ -1742,16 +1702,9 @@ class Eynollah: prediction_regions_org = self.do_prediction(True, img, model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) - ##plt.imshow(prediction_regions_org[:,:,0]) - ##plt.show() prediction_regions_org=prediction_regions_org[:,:,0] prediction_regions_org[(prediction_regions_org[:,:]==1) & (mask_zeros_y[:,:]==1)]=0 - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() if not self.dir_in: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p2) @@ -1763,11 +1716,6 @@ class Eynollah: prediction_regions_org2 = self.do_prediction(True, img, model_region, 0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() mask_zeros2 = (prediction_regions_org2[:,:,0] == 0) mask_lines2 = (prediction_regions_org2[:,:,0] == 3) @@ -1788,8 +1736,6 @@ class Eynollah: mask_lines_only=(prediction_regions_org[:,:]==3)*1 prediction_regions_org = cv2.erode(prediction_regions_org[:,:], KERNEL, iterations=2) - #plt.imshow(text_region2_1st_channel) - #plt.show() prediction_regions_org = cv2.dilate(prediction_regions_org[:,:], KERNEL, iterations=2) @@ -1811,11 +1757,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - if not self.dir_in: - session_bin.close() - del model_bin - del session_bin - gc.collect() if not self.dir_in: @@ -1834,11 +1775,6 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] mask_lines_only=(prediction_regions_org[:,:]==3)*1 - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() mask_texts_only=(prediction_regions_org[:,:]==1)*1 @@ -1859,19 +1795,11 @@ class Eynollah: text_regions_p_true=cv2.fillPoly(text_regions_p_true,pts=polygons_of_only_texts, color=(1,1,1)) - if not self.dir_in: - K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml except: if self.input_binary: prediction_bin = np.copy(img_org) - else: - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) @@ -1887,12 +1815,6 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - if not self.dir_in: - session_bin.close() - del model_bin - del session_bin - gc.collect() if not self.dir_in: @@ -1910,11 +1832,6 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] #mask_lines_only=(prediction_regions_org[:,:]==3)*1 - if not self.dir_in: - session_region.close() - del model_region - del session_region - gc.collect() #img = resize_image(img_org, int(img_org.shape[0]*1), int(img_org.shape[1]*1)) @@ -1925,12 +1842,6 @@ class Eynollah: #prediction_regions_org = prediction_regions_org[:,:,0] #prediction_regions_org[(prediction_regions_org[:,:] == 1) & (mask_zeros_y[:,:] == 1)]=0 - #session_region.close() - #del model_region - #del session_region - #gc.collect() - - mask_lines_only = (prediction_regions_org[:,:] ==3)*1 @@ -1957,8 +1868,6 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) erosion_hurts = True - if not self.dir_in: - K.clear_session() return text_regions_p_true, erosion_hurts, polygons_lines_xml def do_order_of_regions_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -2515,10 +2424,6 @@ class Eynollah: prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) - - del model_region - del session_region - gc.collect() return prediction_table_erode.astype(np.int16) @@ -2619,8 +2524,7 @@ class Eynollah: self.logger.info("Resizing and enhancing image...") is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier(light_version) self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') - if not self.dir_in: - K.clear_session() + scale = 1 if is_image_enhanced: if self.allow_enhancement: @@ -2646,8 +2550,6 @@ class Eynollah: textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) - if not self.dir_in: - K.clear_session() if self.plotter: self.plotter.save_plot_of_textlines(textline_mask_tot_ea, image_page) return textline_mask_tot_ea @@ -2660,7 +2562,7 @@ class Eynollah: if self.plotter: self.plotter.save_deskewed_image(slope_deskew) - self.logger.info("slope_deskew: %s", slope_deskew) + self.logger.info("slope_deskew: %.2f°", slope_deskew) return slope_deskew, slope_first def run_marginals(self, image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction): @@ -2709,8 +2611,6 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - if not self.dir_in: - K.clear_session() self.logger.info("num_col_classifier: %s", num_col_classifier) @@ -2775,8 +2675,6 @@ class Eynollah: pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) - if not self.dir_in: - K.clear_session() self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables @@ -2807,9 +2705,6 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) - if not self.dir_in: - K.clear_session() - gc.collect() if num_col_classifier>=3: if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -2875,38 +2770,22 @@ class Eynollah: text_regions_p[:, :][text_regions_p[:, :] == 2] = 5 text_regions_p[:, :][text_regions_p[:, :] == 3] = 6 text_regions_p[:, :][text_regions_p[:, :] == 4] = 8 - if not self.dir_in: - K.clear_session() + image_page = image_page.astype(np.uint8) regions_fully, regions_fully_only_drop = self.extract_text_regions(image_page, True, cols=num_col_classifier) text_regions_p[:,:][regions_fully[:,:,0]==6]=6 regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 - if not self.dir_in: - K.clear_session() - # plt.imshow(regions_fully[:,:,0]) - # plt.show() regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully) - # plt.imshow(regions_fully[:,:,0]) - # plt.show() - if not self.dir_in: - K.clear_session() + regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) - # plt.imshow(regions_fully_np[:,:,0]) - # plt.show() if num_col_classifier > 2: regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 else: regions_fully_np = filter_small_drop_capitals_from_no_patch_layout(regions_fully_np, text_regions_p) - # plt.imshow(regions_fully_np[:,:,0]) - # plt.show() - if not self.dir_in: - K.clear_session() - # plt.imshow(regions_fully[:,:,0]) - # plt.show() regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) # plt.imshow(regions_fully[:,:,0]) # plt.show() @@ -2929,8 +2808,6 @@ class Eynollah: regions_without_separators_d = None if not self.tables: regions_without_separators = (text_regions_p[:, :] == 1) * 1 - if not self.dir_in: - K.clear_session() img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) self.logger.debug('exit run_boxes_full_layout') @@ -3025,13 +2902,12 @@ class Eynollah: contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) #self.logger.info('areas_cnt_text %s', areas_cnt_text) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] - + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) @@ -3042,14 +2918,14 @@ class Eynollah: contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - areas_cnt_text_d = np.array([cv2.contourArea(contours_only_text_parent_d[j]) for j in range(len(contours_only_text_parent_d))]) + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d=np.argsort(areas_cnt_text_d) - contours_only_text_parent_d=list(np.array(contours_only_text_parent_d)[index_con_parents_d] ) - areas_cnt_text_d=list(np.array(areas_cnt_text_d)[index_con_parents_d] ) + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d)[index_con_parents_d]) + areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) @@ -3103,12 +2979,12 @@ class Eynollah: contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(contours_only_text_parent[j]) for j in range(len(contours_only_text_parent))]) + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [contours_only_text_parent[jz] for jz in range(len(contours_only_text_parent)) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > min_con_area] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) @@ -3146,8 +3022,6 @@ class Eynollah: all_found_texline_polygons = small_textlines_to_parent_adherence2(all_found_texline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_texline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_texline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - if not self.dir_in: - K.clear_session() if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: @@ -3167,8 +3041,6 @@ class Eynollah: if self.plotter: self.plotter.save_plot_of_layout(text_regions_p, image_page) self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - if not self.dir_in: - K.clear_session() pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) From abb0b293f549e6443eced4ce56bb86a9660b9b36 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Sun, 2 Apr 2023 14:07:51 +0200 Subject: [PATCH 100/412] use find_namespace_packages in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9abf158..f4dc6b1 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_namespace_packages, find_packages, setup from json import load install_requires = open('requirements.txt').read().split('\n') From 456fccb35e184db1e7a0e73965e446ed8993f41a Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 12 Apr 2023 23:59:46 +0200 Subject: [PATCH 101/412] use the SavedModel format --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 8706995..525e6c3 100644 --- a/Makefile +++ b/Makefile @@ -22,14 +22,14 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' + # tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz - # tar xf 2022-04-05.SavedModel.tar.gz + tar xf 2022-04-05.SavedModel.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' + wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # Install with pip install: From 63d996880d42a6b49b0fa0d48f3c69b902f72d43 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 13 Apr 2023 00:42:04 +0200 Subject: [PATCH 102/412] include 3.8 in GitHub Actions --- .github/workflows/test-eynollah.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index de742f1..e06cb35 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7'] # '3.8' + python-version: ['3.7', '3.8'] steps: - uses: actions/checkout@v2 From f264eaf424237e11a5d1f2d6199a2d0805eb37af Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 13 Apr 2023 12:28:03 +0200 Subject: [PATCH 103/412] test CircleCI machine executor (more RAM?) --- .circleci/config.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 23eb724..4ae0994 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,8 +3,8 @@ version: 2 jobs: build-python37: - docker: - - image: python:3.7 + machine: + - image: ubuntu-2004:2023.02.1 steps: - checkout - restore_cache: @@ -20,8 +20,8 @@ jobs: - run: make smoke-test build-python38: - docker: - - image: python:3.8 + machine: + - image: ubuntu-2004:2023.02.1 steps: - checkout - restore_cache: @@ -40,6 +40,5 @@ workflows: version: 2 build: jobs: - #- build-python36 - build-python37 - build-python38 From 0462ae0b975f2d7827aeb7cf1648cab4e559e1d3 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:31:14 +0200 Subject: [PATCH 104/412] Update config.yml --- .circleci/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ae0994..092a37c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,9 @@ jobs: paths: models_eynollah.tar.gz models_eynollah + - run: + name: "Set Python Version" + command: pyenv install -s 3.7.16 && pyenv global 3.7.16 - run: make install - run: make smoke-test @@ -33,6 +36,9 @@ jobs: paths: models_eynollah.tar.gz models_eynollah + - run: + name: "Set Python Version" + command: pyenv install -s 3.8.16 && pyenv global 3.8.16 - run: make install - run: make smoke-test From cb8cfad76153bd23ac0fe0f1d0e23bb9dd81a546 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:35:46 +0200 Subject: [PATCH 105/412] Update config.yml --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 092a37c..751ea54 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,8 +17,8 @@ jobs: models_eynollah.tar.gz models_eynollah - run: - name: "Set Python Version" - command: pyenv install -s 3.7.16 && pyenv global 3.7.16 + name: "Set Python Version" + command: pyenv install -s 3.7.16 && pyenv global 3.7.16 - run: make install - run: make smoke-test @@ -37,8 +37,8 @@ jobs: models_eynollah.tar.gz models_eynollah - run: - name: "Set Python Version" - command: pyenv install -s 3.8.16 && pyenv global 3.8.16 + name: "Set Python Version" + command: pyenv install -s 3.8.16 && pyenv global 3.8.16 - run: make install - run: make smoke-test From 8fe35671237ee86c3ec94db18df018dbc972aab3 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:02:41 +0200 Subject: [PATCH 106/412] set_memory_growth to all GPU devices alike --- qurator/eynollah/eynollah.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 264bb62..9312c42 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -496,7 +496,8 @@ class Eynollah: #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) physical_devices = tf.config.list_physical_devices('GPU') try: - tf.config.experimental.set_memory_growth(physical_devices[0], True) + for device in physical_devices: + tf.config.experimental.set_memory_growth(device, True) except: self.logger.warning("no GPU device available") if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): From c251c4f4c80bd02c284c7f93712374016e006658 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 02:11:51 +0200 Subject: [PATCH 107/412] update badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e400d51..f51012e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Eynollah > Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) -[![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=svg)](https://circleci.com/gh/qurator-spk/eynollah) [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) +[![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=shield)](https://circleci.com/gh/qurator-spk/eynollah) +[![GH Actions Test](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml/badge.svg)](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml) [![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://opensource.org/license/apache-2-0/) ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) From 50b9ce3350661ac6d3e7f64be3df792f4a0a3d24 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 02:48:42 +0200 Subject: [PATCH 108/412] Update README.md --- README.md | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f51012e..7ce0782 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Eynollah -> Perform document layout analysis (segmentation) from image data and return the results as [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) +> Document Layout Analysis (segmentation) using pre-trained models and heuristics [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) [![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=shield)](https://circleci.com/gh/qurator-spk/eynollah) @@ -8,24 +8,38 @@ ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) +## Features +* Support for up to 10 segmentation classes: + * background, page border, text region, text line, header, image, separator, marginalia, initial (drop capital), table +* Support for various image optimization operations: + * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing +* Text line segmentation to bounding boxes or polygons (contours) including curved lines and vertical text +* Detection of reading order +* Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) format + ## Installation -`pip install .` or +Python versions `3.7-3.10` with Tensorflow `>=2.4` are currently supported. -`pip install -e .` for editable installation +For (minimal) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit `>=10.1` needs to be installed. -Alternatively, you can also use `make` with these targets: +You can either install via -`make install` or +``` +pip install eynollah +``` -`make install-dev` for editable installation +or clone the repository, enter it and install (editable) with -The current version of Eynollah runs on Python `>=3.7` with Tensorflow `>=2.4`. +``` +git clone git@github.com:qurator-spk/eynollah.git +cd eynollah; pip install -e . +``` -In order to use a GPU for inference, the CUDA toolkit version 10.x needs to be installed. +Alternatively, you can run `make install` or `make install-dev` for editable installation. ### Models -In order to run this tool you need trained models. You can download our pretrained models from [qurator-data.de](https://qurator-data.de/eynollah/). +Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). Alternatively, running `make models` will download and extract models to `$(PWD)/models_eynollah`. @@ -38,7 +52,11 @@ In case you want to train your own model to use with Eynollah, have a look at [s The command-line interface can be called like this: ```sh -eynollah -i -o -m [OPTIONS] +eynollah \ + -i \ + -o \ + -m \ + [OPTIONS] ``` The following options can be used to further configure the processing: @@ -182,5 +200,4 @@ would still use the original (RGB) image despite any binarization that may have
-
- + \ No newline at end of file From d98689edad30cba72749d3abee65d908e6980282 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:13:07 +0200 Subject: [PATCH 109/412] Update README.md --- README.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7ce0782..dd4324a 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ * background, page border, text region, text line, header, image, separator, marginalia, initial (drop capital), table * Support for various image optimization operations: * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing -* Text line segmentation to bounding boxes or polygons (contours) including curved lines and vertical text +* Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text * Detection of reading order -* Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) format +* Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) ## Installation Python versions `3.7-3.10` with Tensorflow `>=2.4` are currently supported. -For (minimal) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit `>=10.1` needs to be installed. +For (limited) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit `>=10.1` needs to be installed. You can either install via @@ -43,8 +43,6 @@ Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data Alternatively, running `make models` will download and extract models to `$(PWD)/models_eynollah`. -### Training - In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). ## Usage @@ -61,22 +59,21 @@ eynollah \ The following options can be used to further configure the processing: -``` --fl perform full layout analysis including detection of headers and drop capitals --tab try to detect tables --light apply a faster but simpler method for main region detection and deskewing --ae allow resizing and enhancing the input image, the enhanced image is saved to the output directory --as allow scaling - automatically check whether the input image needs scaling or not --ib allow binarization of the input image --ho ignore headers for reading order prediction --cl extract contours of curved textlines instead of rectangle bounding boxes --ep enables plotting. This MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae` options --di process all images in a directory in batch mode --si save image regions detected in documents to this directory --sd save deskewed image to this directory --sl save layout prediction as plot to this directory --sa save all outputs (plot, enhanced or binary image and layout prediction) to this directory -``` +| option | description | +|----------|:-------------| +| `-fl` | apply full layout analysis including all steps and segmentation classes | +| `-light` | apply a lighter and faster but simpler method for main region detection and deskewing | +| `-tab` | apply table detection | +| `-ae` | apply enhancement (the resulting image is saved to the output directory) | +| `-as` | apply scaling | +| `-ib` | apply binarization (the resulting image is saved to the output directory) | +| `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | +| `-ho` | ignore headers for reading order dectection | +| `-di ` | process all images in a directory in batch mode | +| `-si ` | save image regions detected in documents to this directory | +| `-sd ` | save deskewed image to this directory | +| `-sl ` | save layout prediction as plot to this directory | +| `-sa ` | save all (plot, enhanced, binary image and layout prediction) to this directory | The tool performs better with RGB images as input than with greyscale or binarized images. From 000e39c676ade539c5cb14bdbf7e64413ae71b59 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:15:45 +0200 Subject: [PATCH 110/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd4324a..85816d1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Features * Support for up to 10 segmentation classes: - * background, page border, text region, text line, header, image, separator, marginalia, initial (drop capital), table + * background, page border, text region, text line, header, image, separator, marginalia, initial, table * Support for various image optimization operations: * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing * Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text From fef7cf309b62b53dc153d01c17eba7a83af7b4d3 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:21:24 +0200 Subject: [PATCH 111/412] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 85816d1..c6c6b2e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The following options can be used to further configure the processing: | `-tab` | apply table detection | | `-ae` | apply enhancement (the resulting image is saved to the output directory) | | `-as` | apply scaling | +| `-cl` | apply polygonal countour detection for curved text lines | | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | | `-ho` | ignore headers for reading order dectection | @@ -73,6 +74,7 @@ The following options can be used to further configure the processing: | `-si ` | save image regions detected in documents to this directory | | `-sd ` | save deskewed image to this directory | | `-sl ` | save layout prediction as plot to this directory | +| `-sp ` | save cropped page image to this directory | | `-sa ` | save all (plot, enhanced, binary image and layout prediction) to this directory | The tool performs better with RGB images as input than with greyscale or binarized images. From 1e172cca5dff556d9b0ce7d68382c34cdc98e536 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:25:01 +0200 Subject: [PATCH 112/412] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c6c6b2e..35d36ca 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ * Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text * Detection of reading order * Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) +* [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface ## Installation Python versions `3.7-3.10` with Tensorflow `>=2.4` are currently supported. From 70786377dcb0232180f69378ffea52e26cd9c476 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:33:01 +0200 Subject: [PATCH 113/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35d36ca..d5505ad 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Features * Support for up to 10 segmentation classes: - * background, page border, text region, text line, header, image, separator, marginalia, initial, table + * background, [page border](https://ocr-d.de/en/gt-guidelines/trans/lyRand.html), [text region](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html), [text line](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html), [header](https://ocr-d.de/en/gt-guidelines/trans/lyUeberschrift.html), [image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html), [separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html), [marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html), [initial](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html), [table](https://ocr-d.de/en/gt-guidelines/trans/lyTabellen.html) * Support for various image optimization operations: * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing * Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text From cb5ffaee141989df23fddf8d6e6824205e99bddc Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 14 Apr 2023 13:24:13 +0200 Subject: [PATCH 114/412] Update README.md --- README.md | 130 ++++++------------------------------------------------ 1 file changed, 14 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index d5505ad..4b7be73 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ The following options can be used to further configure the processing: | `-fl` | apply full layout analysis including all steps and segmentation classes | | `-light` | apply a lighter and faster but simpler method for main region detection and deskewing | | `-tab` | apply table detection | -| `-ae` | apply enhancement (the resulting image is saved to the output directory) | +| `-ae` | apply enhancement and adapt coordinates (the resulting image is saved to the output directory) | | `-as` | apply scaling | -| `-cl` | apply polygonal countour detection for curved text lines | +| `-cl` | apply polygonal countour detection for curved text lines instead of rectangular bounding boxes | | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | | `-ho` | ignore headers for reading order dectection | @@ -78,126 +78,24 @@ The following options can be used to further configure the processing: | `-sp ` | save cropped page image to this directory | | `-sa ` | save all (plot, enhanced, binary image and layout prediction) to this directory | -The tool performs better with RGB images as input than with greyscale or binarized images. +If no option is set, the tool will perform layout detection of main regions (background, text, images, separators and marginals). -## Documentation - -
- click to expand/collapse - -### Region types - -
- click to expand/collapse
- -Eynollah can currently be used to detect the following region types/elements: -* [Border](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_BorderType.html) -* [Textregion](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html) -* [Textline](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html) -* [Image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html) -* [Separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html) -* [Marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html) -* [Initial (Drop Capital)](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html) - -In addition, the tool can detect the [ReadingOrder](https://ocr-d.de/en/gt-guidelines/trans/lyLeserichtung.html) of regions. The final goal is to feed the output to an OCR model. - -
- -### Method description - -
- click to expand/collapse
- -Eynollah uses a combination of various models and heuristics (see flowchart below for the different stages and how they interact): -* [Border detection](https://github.com/qurator-spk/eynollah#border-detection) -* [Layout detection](https://github.com/qurator-spk/eynollah#layout-detection) -* [Textline detection](https://github.com/qurator-spk/eynollah#textline-detection) -* [Image enhancement](https://github.com/qurator-spk/eynollah#Image_enhancement) -* [Scale classification](https://github.com/qurator-spk/eynollah#Scale_classification) -* [Heuristic methods](https://https://github.com/qurator-spk/eynollah#heuristic-methods) - -The first three stages are based on [pixel-wise segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). - -![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) - -#### Border detection -For the purpose of text recognition (OCR) and in order to avoid noise being introduced from texts outside the printspace, one first needs to detect the border of the printed frame. This is done by a binary pixel-wise-segmentation model trained on a dataset of 2,000 documents where about 1,200 of them come from the [dhSegment](https://github.com/dhlab-epfl/dhSegment/) project (you can download the dataset from [here](https://github.com/dhlab-epfl/dhSegment/releases/download/v0.2/pages.zip)) and the remainder having been annotated in SBB. For border detection, the model needs to be fed with the whole image at once rather than separated in patches. - -### Layout detection -As a next step, text regions need to be identified by means of layout detection. Again a pixel-wise segmentation model was trained on 131 labeled images from the SBB digital collections, including some data augmentation. Since the target of this tool are historical documents, we consider as main region types text regions, separators, images, tables and background - each with their own subclasses, e.g. in the case of text regions, subclasses like header/heading, drop capital, main body text etc. While it would be desirable to detect and classify each of these classes in a granular way, there are also limitations due to having a suitably large and balanced training set. Accordingly, the current version of this tool is focussed on the main region types background, text region, image and separator. - -#### Textline detection -In a subsequent step, binary pixel-wise segmentation is used again to classify pixels in a document that constitute textlines. For textline segmentation, a model was initially trained on documents with only one column/block of text and some augmentation with regard to scaling. By fine-tuning the parameters also for multi-column documents, additional training data was produced that resulted in a much more robust textline detection model. - -#### Image enhancement -This is an image to image model which input was low quality of an image and label was actually the original image. For this one we did not have any GT, so we decreased the quality of documents in SBB and then feed them into model. - -#### Scale classification -This is simply an image classifier which classifies images based on their scales or better to say based on their number of columns. - -### Heuristic methods -Some heuristic methods are also employed to further improve the model predictions: -* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. -* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. -* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. -* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. -* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. -* Finally, using the derived coordinates, bounding boxes are determined for each textline. - -
- -### How to use - -
- click to expand/collapse
- -Eynollah makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. - -* If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. - -* If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi (pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. - -* For some documents, while the quality is good, their scale is very large, and the performance of tool decreases. In such cases you can set `-as` (**a**llow **s**caling) to `true`. With this option enabled, the tool will try to rescale the image and only then the layout detection process will begin. - -* If you care about drop capitals (initials) and headings, you can set `-fl` (**f**ull **l**ayout) to `true`. With this setting, the tool can currently distinguish 7 document layout classes/elements. - -* In cases where the document includes curved headers or curved lines, rectangular bounding boxes for textlines will not be a great option. In such cases it is strongly recommended setting the flag `-cl` (**c**urved **l**ines) to `true` to find contours of curved lines instead of rectangular bounding boxes. Be advised that enabling this option increases the processing time of the tool. - -* To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide a directory path to store the extracted images. - -* This tool is actively being developed. If problems occur, or the performance does not meet your expectations, we welcome your feedback via [issues](https://github.com/qurator-spk/eynollah/issues). - -#### `--full-layout` vs `--no-full-layout` - -Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: - -| | `--full-layout` | `--no-full-layout` | -| --- | --- | --- | -| reading order | x | x | -| header regions | x | - | -| text regions | x | x | -| text regions / text line | x | x | -| drop-capitals | x | - | -| marginals | x | x | -| marginals / text line | x | x | -| image region | x | x | +The tool produces better output from RGB images as input than greyscale or binarized images. #### Use as OCR-D processor -Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. In this case, the source image file group with (preferably) RGB images should be used as input like this: +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. -`ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models` - -In fact, the image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. calling +In this case, the source image file group with (preferably) RGB images should be used as input like this: -`ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models` +``` +ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models +``` -would still use the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps +Any image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. calling - #### Eynollah "light" +``` +ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models +``` - Eynollah light uses a faster method to predict and extract the early layout. But with the light option enabled deskewing is not applied for any text region and done only once for the whole document. - -
- -
\ No newline at end of file +still uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps From 529f2c0e19cd99da9735be6321da06657954d355 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:02:41 +0200 Subject: [PATCH 115/412] set_memory_growth to all GPU devices alike --- qurator/eynollah/eynollah.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c9e6674..8444995 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -650,7 +650,8 @@ class Eynollah: #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) physical_devices = tf.config.list_physical_devices('GPU') try: - tf.config.experimental.set_memory_growth(physical_devices[0], True) + for device in physical_devices: + tf.config.experimental.set_memory_growth(device, True) except: self.logger.warning("no GPU device available") From 29e6ad076fdad494414bae6fb89ce8f59fc60ff8 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 18 Apr 2023 13:47:43 +0200 Subject: [PATCH 116/412] renaming textline light model --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8444995..1346672 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -224,7 +224,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425.h5" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314.h5" if self.textline_light: - self.model_textline_dir = dir_models + "/model_17.h5" + self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425.h5" else: self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" self.model_tables = dir_models + "/eynollah-tables_20210319.h5" From 380f59ad675717145e4512c5ed9b9d361c4d6249 Mon Sep 17 00:00:00 2001 From: vahid Date: Tue, 18 Apr 2023 15:06:18 +0200 Subject: [PATCH 117/412] let hybrid textline light model be loaded --- qurator/eynollah/eynollah.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 1346672..4fecfed 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -665,8 +665,12 @@ class Eynollah: if model_dir in self.models: model = self.models[model_dir] else: - model = load_model(model_dir, compile=False) - self.models[model_dir] = model + try: + model = load_model(model_dir, compile=False) + self.models[model_dir] = model + except: + model = load_model(model_dir , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) + self.models[model_dir] = model return model, None From d68f240b59cf6a47091d990d05cd74ba521d054c Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 27 Apr 2023 17:05:21 +0200 Subject: [PATCH 118/412] loading TensorFlow SavedModel format is now present --- qurator/eynollah/eynollah.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 4fecfed..ec65361 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -213,21 +213,21 @@ class Eynollah: self.logger = logger if logger else getLogger('eynollah') self.dir_models = dir_models - self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425.h5" - self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425.h5" - self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425.h5" - self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425.h5" - self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425.h5" - self.model_region_dir_fully_np = dir_models + "/eynollah-full-regions-1column_20210425.h5" - self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425.h5" - self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425.h5" - self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425.h5" - self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314.h5" + self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425" + self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425" + self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" + self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" + self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" + self.model_region_dir_fully_np = dir_models + "/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" + self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" + self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" + self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" if self.textline_light: - self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425.h5" + self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: - self.model_textline_dir = dir_models + "/eynollah-textline_20210425.h5" - self.model_tables = dir_models + "/eynollah-tables_20210319.h5" + self.model_textline_dir = dir_models + "/eynollah-textline_20210425" + self.model_tables = dir_models + "/eynollah-tables_20210319" self.models = {} @@ -1824,6 +1824,9 @@ class Eynollah: if not self.dir_in: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + + else: + prediction_bin = np.copy(img_org) ratio_y=1 ratio_x=1 From 4c217018ccdff3a409aa4a8ff35fca02eedd3dca Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 27 Apr 2023 21:07:33 +0200 Subject: [PATCH 119/412] textline light version -tll can not work without enabling -light option --- qurator/eynollah/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index ddf986e..8c42f64 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -164,6 +164,9 @@ def main( elif enable_plotting and not (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa, -sp, -si or -ae") sys.exit(1) + if textline_light and not light_version: + print('Error: You used -tll to enable light textline detection but -light is not enabled') + sys.exit(1) eynollah = Eynollah( image_filename=image, dir_out=out, From 48f2ce62034bbabefe909820ad8b9cee2ebda47f Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Sat, 13 May 2023 02:39:18 +0200 Subject: [PATCH 120/412] re-enable Action for Python 3.8 --- .github/workflows/test-eynollah.yml | 2 +- README.md | 31 ++++++++++------------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index de742f1..e06cb35 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7'] # '3.8' + python-version: ['3.7', '3.8'] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 1b3a589..07bb411 100644 --- a/README.md +++ b/README.md @@ -38,20 +38,7 @@ cd eynollah; pip install -e . Alternatively, you can run `make install` or `make install-dev` for editable installation. -
- click to expand/collapse
- -First, this model makes use of up to 9 trained models which are responsible for different operations like size detection, column classification, image enhancement, page extraction, main layout detection, full layout detection and textline detection.That does not mean that all 9 models are always required for every document. Based on the document characteristics and parameters specified, different scenarios can be applied. - -Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). - - -* If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi (pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. - -In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). - ## Usage - The command-line interface can be called like this: ```sh @@ -66,26 +53,31 @@ The following options can be used to further configure the processing: | option | description | |----------|:-------------| -| `-fl` | apply full layout analysis including all steps and segmentation classes | -| `-light` | apply a lighter and faster but simpler method for main region detection and deskewing | +| `-fl` | full layout analysis including all steps and segmentation classes | +| `-light` | lighter and faster but simpler method for main region detection and deskewing | | `-tab` | apply table detection | -| `-ae` | apply enhancement and adapt coordinates (the resulting image is saved to the output directory) | +| `-ae` | apply enhancement (the resulting image is saved to the output directory) | | `-as` | apply scaling | -| `-cl` | apply polygonal countour detection for curved text lines instead of rectangular bounding boxes | +| `-cl` | apply countour detection for curved text lines instead of bounding boxes | | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | | `-ho` | ignore headers for reading order dectection | | `-di ` | process all images in a directory in batch mode | -| `-si ` | save image regions detected in documents to this directory | +| `-si ` | save image regions detected to this directory | | `-sd ` | save deskewed image to this directory | | `-sl ` | save layout prediction as plot to this directory | | `-sp ` | save cropped page image to this directory | -| `-sa ` | save all (plot, enhanced, binary image and layout prediction) to this directory | +| `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | If no option is set, the tool will perform layout detection of main regions (background, text, images, separators and marginals). The tool produces better output from RGB images as input than greyscale or binarized images. +## Models +Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). + +In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + #### Use as OCR-D processor Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. @@ -103,4 +95,3 @@ ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models ``` still uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps - From 6eab7a60a9ab0345a71af27cde09fc44d18d4e83 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Sat, 13 May 2023 12:42:05 +0200 Subject: [PATCH 121/412] Update ocrd-tool.json --- qurator/eynollah/ocrd-tool.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index fc9ee72..f9fd7a7 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.2.0", + "version": "0.3.0", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { @@ -53,9 +53,9 @@ "resources": [ { "description": "models for eynollah (TensorFlow format)", - "url": "https://qurator-data.de/eynollah/2021-04-25/SavedModel.tar.gz", + "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz", "name": "default", - "size": 1483106598, + "size": 1757668443, "type": "archive", "path_in_archive": "default" } From 419e589df755f3ee6d22194d920d7d28c1587173 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Sat, 13 May 2023 12:44:45 +0200 Subject: [PATCH 122/412] Update CHANGELOG.md --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6ceff..da2e1c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.3.0] - 2023-05-13 + +Changed: + + * Eynollah light integration, #86 + * use PEP420 style qurator namespace, #97 + * set_memory_growth to all GPU devices alike, #100 + +Fixed: + + * PAGE-XML coordinates can have self-intersections, #20 + * reading order representation (XML order vs index), #22 + * allow cropping separately, #26 + * Order of regions, #51 + * error while running inference, #75 + * Eynollah crashes while processing image, #77 + * ValueError: bad marshal data, #87 + * contour extraction: inhomogeneous shape, #92 + * Confusing model dir variables, #93 + * New release?, #96 + ## [0.2.0] - 2023-03-24 Changed: From c7057bad5e9bbacd867b0225012b625bd819e846 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Sat, 13 May 2023 12:47:06 +0200 Subject: [PATCH 123/412] Update README.md --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 07bb411..80dab77 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ cd eynollah; pip install -e . Alternatively, you can run `make install` or `make install-dev` for editable installation. +## Models +Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). + +In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). + ## Usage The command-line interface can be called like this: @@ -70,13 +75,7 @@ The following options can be used to further configure the processing: | `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | If no option is set, the tool will perform layout detection of main regions (background, text, images, separators and marginals). - -The tool produces better output from RGB images as input than greyscale or binarized images. - -## Models -Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). - -In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). +The tool produces better quality output when RGB images are used as input than greyscale or binarized images. #### Use as OCR-D processor @@ -88,10 +87,10 @@ In this case, the source image file group with (preferably) RGB images should be ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models ``` -Any image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. calling +Any image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. ``` ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models ``` -still uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps +uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps From a96147b621313b2c2127064fc37dd8027703219e Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Sat, 13 May 2023 15:36:24 +0200 Subject: [PATCH 124/412] improve links to GT guidelines --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80dab77..ef6515c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Features * Support for up to 10 segmentation classes: - * background, [page border](https://ocr-d.de/en/gt-guidelines/trans/lyRand.html), [text region](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextRegionType.html), [text line](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html), [header](https://ocr-d.de/en/gt-guidelines/trans/lyUeberschrift.html), [image](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_ImageRegionType.html), [separator](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_SeparatorRegionType.html), [marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html), [initial](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html), [table](https://ocr-d.de/en/gt-guidelines/trans/lyTabellen.html) + * background, [page border](https://ocr-d.de/en/gt-guidelines/trans/lyRand.html), [text region](https://ocr-d.de/en/gt-guidelines/trans/lytextregion.html#textregionen__textregion_), [text line](https://ocr-d.de/en/gt-guidelines/pagexml/pagecontent_xsd_Complex_Type_pc_TextLineType.html), [header](https://ocr-d.de/en/gt-guidelines/trans/lyUeberschrift.html), [image](https://ocr-d.de/en/gt-guidelines/trans/lyBildbereiche.html), [separator](https://ocr-d.de/en/gt-guidelines/trans/lySeparatoren.html), [marginalia](https://ocr-d.de/en/gt-guidelines/trans/lyMarginalie.html), [initial](https://ocr-d.de/en/gt-guidelines/trans/lyInitiale.html), [table](https://ocr-d.de/en/gt-guidelines/trans/lyTabellen.html) * Support for various image optimization operations: * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing * Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text From 45c40a58fcfb24935f8a8469e350e3fad66a0573 Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 19 May 2023 14:48:56 +0200 Subject: [PATCH 125/412] issue #67 solved --- qurator/eynollah/eynollah.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index fa55055..ceea2fe 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -452,6 +452,9 @@ class Eynollah: if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) num_column_is_classified = False + elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + img_new = np.copy(img) + num_column_is_classified = False else: img_new = resize_image(img, img_h_new, img_w_new) num_column_is_classified = True @@ -2831,7 +2834,7 @@ class Eynollah: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - + print(img_res.shape) self.logger.info("Enhancing took %.1fs ", time.time() - t0) t1 = time.time() From b01888da31818c30386b7227f0601f5f45343dc3 Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 19 May 2023 14:50:59 +0200 Subject: [PATCH 126/412] delete printing resized image shape --- qurator/eynollah/eynollah.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ceea2fe..a408b42 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2834,7 +2834,6 @@ class Eynollah: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - print(img_res.shape) self.logger.info("Enhancing took %.1fs ", time.time() - t0) t1 = time.time() From 0cda1f3c7a79b76f9828a903981467dd0249419a Mon Sep 17 00:00:00 2001 From: vahid Date: Fri, 26 May 2023 15:08:27 +0200 Subject: [PATCH 127/412] reading order type 1: right to left --- qurator/eynollah/eynollah.py | 20 ++- qurator/eynollah/utils/__init__.py | 252 ++++++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 9 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index a408b42..1a5705d 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -72,7 +72,8 @@ from .utils import ( small_textlines_to_parent_adherence2, order_of_regions, find_number_of_columns_in_document, - return_boxes_of_images_by_order_of_reading_new) + return_boxes_of_images_by_order_of_reading_new, + return_boxes_of_images_by_order_of_reading_new_right2left) from .utils.pil_cv2 import check_dpi, pil2cv from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter @@ -2069,6 +2070,7 @@ class Eynollah: arg_text_con = [] for ii in range(len(cx_text_only)): for jj in range(len(boxes)): + print(cx_text_only[ii],cy_text_only[ii],'markaz') if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) break @@ -2104,6 +2106,9 @@ class Eynollah: ref_point += len(id_of_texts) order_of_texts_tot = [] + print(len(contours_only_text_parent),'contours_only_text_parent') + print(len(order_by_con_main),'order_by_con_main') + for tj1 in range(len(contours_only_text_parent)): order_of_texts_tot.append(int(order_by_con_main[tj1])) @@ -2618,7 +2623,7 @@ class Eynollah: regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) @@ -2628,7 +2633,7 @@ class Eynollah: img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) @@ -2713,7 +2718,7 @@ class Eynollah: pass if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) text_regions_p_tables = np.copy(text_regions_p) text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 pixel_line = 3 @@ -2722,7 +2727,7 @@ class Eynollah: img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) text_regions_p_tables = np.copy(text_regions_p_1_n) text_regions_p_tables = np.round(text_regions_p_tables) text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 @@ -3065,9 +3070,10 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index e9f872c..c59d508 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -1774,7 +1774,6 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) - if (reading_order_type==1) or (reading_order_type==0 and (len(y_lines_without_mother)>=2 or there_is_sep_with_child==1)): @@ -2281,7 +2280,6 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho ind_args=np.array(range(len(y_type_2))) #ind_args=np.array(ind_args) - #print(ind_args,'ind_args') for column in range(len(peaks_neg_tot)-1): #print(column,'column') ind_args_in_col=ind_args[x_starting==column] @@ -2338,3 +2336,253 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho #else: #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) return boxes, peaks_neg_tot_tables + + + + + + + + + + + + + + + + + + + + +def return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, tables): + boxes=[] + peaks_neg_tot_tables = [] + + for i in range(len(splitter_y_new)-1): + #print(splitter_y_new[i],splitter_y_new[i+1]) + matrix_new=matrix_of_lines_ch[:,:][ (matrix_of_lines_ch[:,6]> splitter_y_new[i] ) & (matrix_of_lines_ch[:,7]< splitter_y_new[i+1] ) ] + #print(len( matrix_new[:,9][matrix_new[:,9]==1] )) + + #print(matrix_new[:,8][matrix_new[:,9]==1],'gaddaaa') + + # check to see is there any vertical separator to find holes. + if 1>0:#len( matrix_new[:,9][matrix_new[:,9]==1] )>0 and np.max(matrix_new[:,8][matrix_new[:,9]==1])>=0.1*(np.abs(splitter_y_new[i+1]-splitter_y_new[i] )): + + try: + if erosion_hurts: + num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], num_col_classifier, tables, multiplier=6.) + else: + num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],num_col_classifier, tables, multiplier=7.) + except: + peaks_neg_fin=[] + num_col = 0 + + + try: + peaks_neg_fin_org=np.copy(peaks_neg_fin) + if (len(peaks_neg_fin)+1)=len(peaks_neg_fin2): + peaks_neg_fin=list(np.copy(peaks_neg_fin1)) + else: + peaks_neg_fin=list(np.copy(peaks_neg_fin2)) + + + + peaks_neg_fin=list(np.array(peaks_neg_fin)+peaks_neg_fin_early[i_n]) + + if i_n!=(len(peaks_neg_fin_early)-2): + peaks_neg_fin_rev.append(peaks_neg_fin_early[i_n+1]) + #print(peaks_neg_fin,'peaks_neg_fin') + peaks_neg_fin_rev=peaks_neg_fin_rev+peaks_neg_fin + + + + + + if len(peaks_neg_fin_rev)>=len(peaks_neg_fin_org): + peaks_neg_fin=list(np.sort(peaks_neg_fin_rev)) + num_col=len(peaks_neg_fin) + else: + peaks_neg_fin=list(np.copy(peaks_neg_fin_org)) + num_col=len(peaks_neg_fin) + + #print(peaks_neg_fin,'peaks_neg_fin') + except: + pass + #num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],multiplier=7.0) + x_min_hor_some=matrix_new[:,2][ (matrix_new[:,9]==0) ] + x_max_hor_some=matrix_new[:,3][ (matrix_new[:,9]==0) ] + cy_hor_some=matrix_new[:,5][ (matrix_new[:,9]==0) ] + cy_hor_diff=matrix_new[:,7][ (matrix_new[:,9]==0) ] + arg_org_hor_some=matrix_new[:,0][ (matrix_new[:,9]==0) ] + + + + + + peaks_neg_tot=return_points_with_boundies(peaks_neg_fin,0, regions_without_separators[:,:].shape[1]) + + peaks_neg_tot_tables.append(peaks_neg_tot) + + reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) + + + y_lines_by_order=[] + x_start_by_order=[] + x_end_by_order=[] + if len(x_starting)>0: + all_columns = np.array(range(len(peaks_neg_tot)-1)) + columns_covered_by_lines_covered_more_than_2col=[] + + for dj in range(len(x_starting)): + if set( list(np.array(range(x_starting[dj],x_ending[dj])) ) ) == set(all_columns): + pass + else: + columns_covered_by_lines_covered_more_than_2col=columns_covered_by_lines_covered_more_than_2col+list(np.array(range(x_starting[dj],x_ending[dj])) ) + columns_covered_by_lines_covered_more_than_2col=list(set(columns_covered_by_lines_covered_more_than_2col)) + + + + columns_not_covered=list( set(all_columns)-set(columns_covered_by_lines_covered_more_than_2col) ) + + + y_type_2=list(y_type_2) + x_starting=list(x_starting) + x_ending=list(x_ending) + + for lj in columns_not_covered: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(lj) + x_ending.append(lj+1) + ##y_lines_by_order.append(int(splitter_y_new[i])) + ##x_start_by_order.append(0) + + #y_type_2.append(int(splitter_y_new[i])) + #x_starting.append(x_starting[0]) + #x_ending.append(x_ending[0]) + + if len(new_main_sep_y)>0: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(0) + x_ending.append(len(peaks_neg_tot)-1) + else: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(x_starting[0]) + x_ending.append(x_ending[0]) + + + y_type_2=np.array(y_type_2) + x_starting=np.array(x_starting) + x_ending=np.array(x_ending) + else: + all_columns=np.array(range(len(peaks_neg_tot)-1)) + columns_not_covered=list( set(all_columns) ) + + + y_type_2=list(y_type_2) + x_starting=list(x_starting) + x_ending=list(x_ending) + + for lj in columns_not_covered: + y_type_2.append(int(splitter_y_new[i])) + x_starting.append(lj) + x_ending.append(lj+1) + ##y_lines_by_order.append(int(splitter_y_new[i])) + ##x_start_by_order.append(0) + + + + y_type_2=np.array(y_type_2) + x_starting=np.array(x_starting) + x_ending=np.array(x_ending) + + ind_args=np.array(range(len(y_type_2))) + #ind_args=np.array(ind_args) + #print(ind_args,'ind_args') + for column in range(len(peaks_neg_tot)-1,0,-1): + #print(column,'column') + ind_args_in_col=ind_args[x_ending==column] + ind_args_in_col=np.array(ind_args_in_col) + #print(len(y_type_2)) + y_column=y_type_2[ind_args_in_col] + x_start_column=x_starting[ind_args_in_col] + x_end_column=x_ending[ind_args_in_col] + + ind_args_col_sorted=np.argsort(y_column) + y_col_sort=y_column[ind_args_col_sorted] + x_start_column_sort=x_start_column[ind_args_col_sorted] + x_end_column_sort=x_end_column[ind_args_col_sorted] + #print('babali4') + for ii in range(len(y_col_sort)): + #print('babali5') + y_lines_by_order.append(y_col_sort[ii]) + x_start_by_order.append(x_start_column_sort[ii]) + x_end_by_order.append(x_end_column_sort[ii]-1) + + for il in range(len(y_lines_by_order)): + + + y_copy=list( np.copy(y_lines_by_order) ) + x_start_copy=list( np.copy(x_start_by_order) ) + x_end_copy=list ( np.copy(x_end_by_order) ) + + #print(y_copy,'y_copy') + y_itself=y_copy.pop(il) + x_start_itself=x_start_copy.pop(il) + x_end_itself=x_end_copy.pop(il) + + #print(y_copy,'y_copy2') + + for column in range(x_end_itself+1-1,x_start_itself-1,-1): + #print(column,'cols') + y_in_cols=[] + for yic in range(len(y_copy)): + #print('burda') + if y_copy[yic]>y_itself and column>=x_start_copy[yic] and column<=x_end_copy[yic]: + y_in_cols.append(y_copy[yic]) + #print('burda2') + #print(y_in_cols,'y_in_cols') + if len(y_in_cols)>0: + y_down=np.min(y_in_cols) + else: + y_down=[int(splitter_y_new[i+1])][0] + #print(y_itself,'y_itself') + boxes.append([peaks_neg_tot[column],peaks_neg_tot[column+1],y_itself,y_down]) + + + + #else: + #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) + return boxes, peaks_neg_tot_tables + From 0b350118470b40cf30eb32d40e22ed644c57a5a8 Mon Sep 17 00:00:00 2001 From: vahid Date: Thu, 1 Jun 2023 17:30:12 +0200 Subject: [PATCH 128/412] right2left reading order detection accomplished --- qurator/eynollah/cli.py | 8 + qurator/eynollah/eynollah.py | 25 ++- qurator/eynollah/utils/__init__.py | 279 +++-------------------------- 3 files changed, 52 insertions(+), 260 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 4bbd3f2..a2a2ad0 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -97,6 +97,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="if this parameter set to true, this tool will try to detect tables.", ) +@click.option( + "--right2left/--left2right", + "-r2l/-l2r", + is_flag=True, + help="if this parameter set to true, this tool will extract right-to-left reading order.", +) @click.option( "--input_binary/--input-RGB", "-ib/-irgb", @@ -149,6 +155,7 @@ def main( textline_light, full_layout, tables, + right2left, input_binary, allow_scaling, headers_off, @@ -184,6 +191,7 @@ def main( textline_light=textline_light, full_layout=full_layout, tables=tables, + right2left=right2left, input_binary=input_binary, allow_scaling=allow_scaling, headers_off=headers_off, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 1a5705d..ad3f312 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -72,8 +72,7 @@ from .utils import ( small_textlines_to_parent_adherence2, order_of_regions, find_number_of_columns_in_document, - return_boxes_of_images_by_order_of_reading_new, - return_boxes_of_images_by_order_of_reading_new_right2left) + return_boxes_of_images_by_order_of_reading_new) from .utils.pil_cv2 import check_dpi, pil2cv from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter @@ -159,6 +158,7 @@ class Eynollah: textline_light=False, full_layout=False, tables=False, + right2left=False, input_binary=False, allow_scaling=False, headers_off=False, @@ -190,6 +190,7 @@ class Eynollah: self.textline_light = textline_light self.full_layout = full_layout self.tables = tables + self.right2left = right2left self.input_binary = input_binary self.allow_scaling = allow_scaling self.headers_off = headers_off @@ -2623,7 +2624,7 @@ class Eynollah: regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) @@ -2633,7 +2634,7 @@ class Eynollah: img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) @@ -2718,7 +2719,7 @@ class Eynollah: pass if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) text_regions_p_tables = np.copy(text_regions_p) text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 pixel_line = 3 @@ -2727,7 +2728,7 @@ class Eynollah: img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) text_regions_p_tables = np.copy(text_regions_p_1_n) text_regions_p_tables = np.round(text_regions_p_tables) text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 @@ -3070,11 +3071,17 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - + #print(boxes_d,'boxes_d') + #img_once = np.zeros((textline_mask_tot_d.shape[0],textline_mask_tot_d.shape[1])) + #for box_i in boxes_d: + #img_once[int(box_i[2]):int(box_i[3]),int(box_i[0]):int(box_i[1]) ] =1 + #plt.imshow(img_once) + #plt.show() + #print(np.unique(img_once),'img_once') if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) t_order = time.time() diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index c59d508..b85abdf 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -1672,7 +1672,9 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, return num_col_fin, peaks_neg_fin_fin,matrix_of_lines_ch,splitter_y_new,separators_closeup_n -def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, tables): +def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, tables, right2left_readingorder): + if right2left_readingorder: + regions_without_separators = cv2.flip(regions_without_separators,1) boxes=[] peaks_neg_tot_tables = [] @@ -1763,6 +1765,13 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho cy_hor_diff=matrix_new[:,7][ (matrix_new[:,9]==0) ] arg_org_hor_some=matrix_new[:,0][ (matrix_new[:,9]==0) ] + if right2left_readingorder: + x_max_hor_some_new = regions_without_separators.shape[1] - x_min_hor_some + x_min_hor_some_new = regions_without_separators.shape[1] - x_max_hor_some + + x_min_hor_some =list(np.copy(x_min_hor_some_new)) + x_max_hor_some =list(np.copy(x_max_hor_some_new)) + @@ -2026,6 +2035,7 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho columns_not_covered_child_no_mother=np.sort(columns_not_covered_child_no_mother) + ind_args=np.array(range(len(y_type_2))) @@ -2335,254 +2345,21 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho #else: #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) - return boxes, peaks_neg_tot_tables - - - - - - - - - - - - - - - - - - - - -def return_boxes_of_images_by_order_of_reading_new_right2left(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, tables): - boxes=[] - peaks_neg_tot_tables = [] - - for i in range(len(splitter_y_new)-1): - #print(splitter_y_new[i],splitter_y_new[i+1]) - matrix_new=matrix_of_lines_ch[:,:][ (matrix_of_lines_ch[:,6]> splitter_y_new[i] ) & (matrix_of_lines_ch[:,7]< splitter_y_new[i+1] ) ] - #print(len( matrix_new[:,9][matrix_new[:,9]==1] )) + + if right2left_readingorder: + peaks_neg_tot_tables_new = [] + if len(peaks_neg_tot_tables)>=1: + for peaks_tab_ind in peaks_neg_tot_tables: + peaks_neg_tot_tables_ind = regions_without_separators.shape[1] - np.array(peaks_tab_ind) + peaks_neg_tot_tables_ind = list(peaks_neg_tot_tables_ind[::-1]) + peaks_neg_tot_tables_new.append(peaks_neg_tot_tables_ind) + - #print(matrix_new[:,8][matrix_new[:,9]==1],'gaddaaa') - - # check to see is there any vertical separator to find holes. - if 1>0:#len( matrix_new[:,9][matrix_new[:,9]==1] )>0 and np.max(matrix_new[:,8][matrix_new[:,9]==1])>=0.1*(np.abs(splitter_y_new[i+1]-splitter_y_new[i] )): - - try: - if erosion_hurts: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], num_col_classifier, tables, multiplier=6.) - else: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],num_col_classifier, tables, multiplier=7.) - except: - peaks_neg_fin=[] - num_col = 0 - - - try: - peaks_neg_fin_org=np.copy(peaks_neg_fin) - if (len(peaks_neg_fin)+1)=len(peaks_neg_fin2): - peaks_neg_fin=list(np.copy(peaks_neg_fin1)) - else: - peaks_neg_fin=list(np.copy(peaks_neg_fin2)) - - - - peaks_neg_fin=list(np.array(peaks_neg_fin)+peaks_neg_fin_early[i_n]) - - if i_n!=(len(peaks_neg_fin_early)-2): - peaks_neg_fin_rev.append(peaks_neg_fin_early[i_n+1]) - #print(peaks_neg_fin,'peaks_neg_fin') - peaks_neg_fin_rev=peaks_neg_fin_rev+peaks_neg_fin - - - - - - if len(peaks_neg_fin_rev)>=len(peaks_neg_fin_org): - peaks_neg_fin=list(np.sort(peaks_neg_fin_rev)) - num_col=len(peaks_neg_fin) - else: - peaks_neg_fin=list(np.copy(peaks_neg_fin_org)) - num_col=len(peaks_neg_fin) - - #print(peaks_neg_fin,'peaks_neg_fin') - except: - pass - #num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],multiplier=7.0) - x_min_hor_some=matrix_new[:,2][ (matrix_new[:,9]==0) ] - x_max_hor_some=matrix_new[:,3][ (matrix_new[:,9]==0) ] - cy_hor_some=matrix_new[:,5][ (matrix_new[:,9]==0) ] - cy_hor_diff=matrix_new[:,7][ (matrix_new[:,9]==0) ] - arg_org_hor_some=matrix_new[:,0][ (matrix_new[:,9]==0) ] - - - - - - peaks_neg_tot=return_points_with_boundies(peaks_neg_fin,0, regions_without_separators[:,:].shape[1]) - - peaks_neg_tot_tables.append(peaks_neg_tot) - - reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) - - - y_lines_by_order=[] - x_start_by_order=[] - x_end_by_order=[] - if len(x_starting)>0: - all_columns = np.array(range(len(peaks_neg_tot)-1)) - columns_covered_by_lines_covered_more_than_2col=[] - - for dj in range(len(x_starting)): - if set( list(np.array(range(x_starting[dj],x_ending[dj])) ) ) == set(all_columns): - pass - else: - columns_covered_by_lines_covered_more_than_2col=columns_covered_by_lines_covered_more_than_2col+list(np.array(range(x_starting[dj],x_ending[dj])) ) - columns_covered_by_lines_covered_more_than_2col=list(set(columns_covered_by_lines_covered_more_than_2col)) - - - - columns_not_covered=list( set(all_columns)-set(columns_covered_by_lines_covered_more_than_2col) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - - #y_type_2.append(int(splitter_y_new[i])) - #x_starting.append(x_starting[0]) - #x_ending.append(x_ending[0]) - - if len(new_main_sep_y)>0: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(0) - x_ending.append(len(peaks_neg_tot)-1) - else: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_starting[0]) - x_ending.append(x_ending[0]) - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) - else: - all_columns=np.array(range(len(peaks_neg_tot)-1)) - columns_not_covered=list( set(all_columns) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) - - ind_args=np.array(range(len(y_type_2))) - #ind_args=np.array(ind_args) - #print(ind_args,'ind_args') - for column in range(len(peaks_neg_tot)-1,0,-1): - #print(column,'column') - ind_args_in_col=ind_args[x_ending==column] - ind_args_in_col=np.array(ind_args_in_col) - #print(len(y_type_2)) - y_column=y_type_2[ind_args_in_col] - x_start_column=x_starting[ind_args_in_col] - x_end_column=x_ending[ind_args_in_col] - - ind_args_col_sorted=np.argsort(y_column) - y_col_sort=y_column[ind_args_col_sorted] - x_start_column_sort=x_start_column[ind_args_col_sorted] - x_end_column_sort=x_end_column[ind_args_col_sorted] - #print('babali4') - for ii in range(len(y_col_sort)): - #print('babali5') - y_lines_by_order.append(y_col_sort[ii]) - x_start_by_order.append(x_start_column_sort[ii]) - x_end_by_order.append(x_end_column_sort[ii]-1) - - for il in range(len(y_lines_by_order)): - - - y_copy=list( np.copy(y_lines_by_order) ) - x_start_copy=list( np.copy(x_start_by_order) ) - x_end_copy=list ( np.copy(x_end_by_order) ) - - #print(y_copy,'y_copy') - y_itself=y_copy.pop(il) - x_start_itself=x_start_copy.pop(il) - x_end_itself=x_end_copy.pop(il) - - #print(y_copy,'y_copy2') - - for column in range(x_end_itself+1-1,x_start_itself-1,-1): - #print(column,'cols') - y_in_cols=[] - for yic in range(len(y_copy)): - #print('burda') - if y_copy[yic]>y_itself and column>=x_start_copy[yic] and column<=x_end_copy[yic]: - y_in_cols.append(y_copy[yic]) - #print('burda2') - #print(y_in_cols,'y_in_cols') - if len(y_in_cols)>0: - y_down=np.min(y_in_cols) - else: - y_down=[int(splitter_y_new[i+1])][0] - #print(y_itself,'y_itself') - boxes.append([peaks_neg_tot[column],peaks_neg_tot[column+1],y_itself,y_down]) - - - - #else: - #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) - return boxes, peaks_neg_tot_tables - + for i in range(len(boxes)): + x_start_new = regions_without_separators.shape[1] - boxes[i][1] + x_end_new = regions_without_separators.shape[1] - boxes[i][0] + boxes[i][0] = x_start_new + boxes[i][1] = x_end_new + return boxes, peaks_neg_tot_tables_new + else: + return boxes, peaks_neg_tot_tables From 69c1d6b3d611bcb45a29b15a043f6098fbdf68dd Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 9 Jun 2023 19:22:34 +0200 Subject: [PATCH 129/412] Revert "Merge pull request #97 from qurator-spk/420-namespace-package" This reverts commit fd56b86acf55677dc7a8bfb9e2737c3cc167327a, reversing changes made to ea792d1e4ac4a722770b82dc91e71f84d5beb212. --- qurator/__init__.py | 1 + setup.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qurator/__init__.py b/qurator/__init__.py index e69de29..5284146 100644 --- a/qurator/__init__.py +++ b/qurator/__init__.py @@ -0,0 +1 @@ +__import__("pkg_resources").declare_namespace(__name__) diff --git a/setup.py b/setup.py index 807eae7..9abf158 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import find_namespace_packages, find_packages, setup +from setuptools import setup, find_packages from json import load install_requires = open('requirements.txt').read().split('\n') @@ -13,6 +13,7 @@ setup( author='Vahid Rezanezhad', url='https://github.com/qurator-spk/eynollah', license='Apache License 2.0', + namespace_packages=['qurator'], packages=find_packages(exclude=['tests']), install_requires=install_requires, package_data={ From b049e475e34b49536527f872abcf765f4010a6d7 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 10 Jun 2023 11:14:27 +0200 Subject: [PATCH 130/412] update tool json resource path_in_archive --- qurator/eynollah/ocrd-tool.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index f9fd7a7..8a2cb95 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -55,9 +55,9 @@ "description": "models for eynollah (TensorFlow format)", "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz", "name": "default", - "size": 1757668443, + "size": 1761991295, "type": "archive", - "path_in_archive": "default" + "path_in_archive": "models_eynollah" } ] } From 867a7261de27bec7efc7e7add80f10ae18bc419a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Mon, 12 Jun 2023 00:45:57 +0200 Subject: [PATCH 131/412] pil_cv2.check_dpi: fix class membership test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (depending on how the `PIL.Image` was instantiated – file plugin or array interface – the previous `isinstance` could fail, provoking a fall-through to `cv2pil` which does not work) --- qurator/eynollah/utils/pil_cv2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/utils/pil_cv2.py b/qurator/eynollah/utils/pil_cv2.py index 20dc22f..83ae47d 100644 --- a/qurator/eynollah/utils/pil_cv2.py +++ b/qurator/eynollah/utils/pil_cv2.py @@ -16,7 +16,7 @@ def pil2cv(img): def check_dpi(img): try: - if isinstance(img, Image.__class__): + if isinstance(img, Image.Image): pil_image = img elif isinstance(img, str): pil_image = Image.open(img) From a0949fd74a437ea204d669ef2a9e6361aa651822 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:54:04 +0200 Subject: [PATCH 132/412] add HIP'23 paper reference --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index ef6515c..8a183c6 100644 --- a/README.md +++ b/README.md @@ -94,3 +94,18 @@ ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models ``` uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps + +## How to cite +If you find this tool useful in your work, please consider citing our paper: + +``` +@article{rezanezhad2023eynollah, + title = {Document Layout Analysis with Deep Learning and Heuristics}, + author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, + booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, + San José, US, August 25-26, 2023, ACM.}, + year = {2023}, + url = {https://doi.org/10.1145/3604951.3605513} +} +``` +``` From aad80696e608c9c76ecebadc0131521257770d00 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:04:24 +0200 Subject: [PATCH 133/412] format citation info as bibtex --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8a183c6..77d7e44 100644 --- a/README.md +++ b/README.md @@ -98,14 +98,13 @@ uses the original (RGB) image despite any binarization that may have occured in ## How to cite If you find this tool useful in your work, please consider citing our paper: -``` -@article{rezanezhad2023eynollah, - title = {Document Layout Analysis with Deep Learning and Heuristics}, - author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, +```bibtex +@inproceedings{rezanezhad2023eynollah, + title = {Document Layout Analysis with Deep Learning and Heuristics}, + author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, - San José, US, August 25-26, 2023, ACM.}, - year = {2023}, - url = {https://doi.org/10.1145/3604951.3605513} + San José, US, August 25-26, 2023, ACM.}, + year = {2023}, + url = {https://doi.org/10.1145/3604951.3605513} } ``` -``` From 2db588a1460b6b6e7a742a0aae3c769b348e2f81 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:39:49 +0200 Subject: [PATCH 134/412] Update bibtex entry --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77d7e44..85a672b 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,11 @@ If you find this tool useful in your work, please consider citing our paper: title = {Document Layout Analysis with Deep Learning and Heuristics}, author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, - San José, US, August 25-26, 2023, ACM.}, + San José, CA, USA, August 25-26, 2023}, + publisher = {Association for Computing Machinery}, + address = {New York, NY, USA}, year = {2023}, + pages = {73--78}, url = {https://doi.org/10.1145/3604951.3605513} } ``` From 6ba5cdcc1191d3f2c7c8840de315cbeb4b2346fc Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Mon, 14 Aug 2023 22:16:51 +0200 Subject: [PATCH 135/412] Update citation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85a672b..d74c4a9 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ uses the original (RGB) image despite any binarization that may have occured in If you find this tool useful in your work, please consider citing our paper: ```bibtex -@inproceedings{rezanezhad2023eynollah, +@inproceedings{rezanezhad2023documentlayoutanalysis, title = {Document Layout Analysis with Deep Learning and Heuristics}, author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, From e5acee09abb3e291fc6b3b48c3655648422cad40 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:05:51 +0200 Subject: [PATCH 136/412] cap tensorflow version to <2.12.0 Cap tensorflow version to <2.12.0 until we have time to adapt to the API changes such as e.g. * Support for Python 3.11 has been added. * Support for Python 3.7 has been removed. See also https://github.com/tensorflow/tensorflow/releases/tag/v2.12.0. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0180d01..0d7c5b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # ocrd includes opencv, numpy, shapely, click ocrd >= 2.23.3 scikit-learn >= 0.23.2 -tensorflow >= 2.4.0 +tensorflow >= 2.4.0, <2.12.0 imutils >= 0.5.3 matplotlib setuptools >= 50 From 935332e86352b86453cdc61fcd548f484a5d3ae3 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 17 Aug 2023 21:45:48 +0200 Subject: [PATCH 137/412] test fix for keras.backend import error with Python 3.8 --- qurator/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ad3f312..57ddcad 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -29,7 +29,7 @@ warnings.filterwarnings("ignore") from scipy.signal import find_peaks import matplotlib.pyplot as plt from scipy.ndimage import gaussian_filter1d -from keras.backend import set_session +from tensorflow.python.keras.backend import set_session from tensorflow.keras import layers from .utils.contour import ( From b58a327c5d53a05ba3142802db27e25dfd7380be Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 17 Aug 2023 22:07:45 +0200 Subject: [PATCH 138/412] cap numpy to <1.24.0 OK so now numpy is the culprit (shipped unbound via ocrd) which had several deprecations expire with release of v1.24.0 that require changes to our codebase, e.g. * The deprecation for the aliases np.object, np.bool, np.float, np.complex, np.str, and np.int is expired * Ragged array creation will now always raise a ValueError unless dtype=object is passed. See also here: https://numpy.org/devdocs/release/1.24.0-notes.html#expired-deprecations --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0d7c5b2..a19a191 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ # ocrd includes opencv, numpy, shapely, click ocrd >= 2.23.3 +numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow >= 2.4.0, <2.12.0 imutils >= 0.5.3 From 3e6265757060a414370d21a5edceb3297a0f5d8b Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:02:10 +0200 Subject: [PATCH 139/412] added the TF upper bound in the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d74c4a9..f17d194 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface ## Installation -Python versions `3.7-3.10` with Tensorflow `>=2.4` are currently supported. +Python versions `3.7-3.10` with Tensorflow versions `2.4-2.11` are currently supported. -For (limited) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit `>=10.1` needs to be installed. +For (limited) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit needs to be installed. You can either install via From d3b06baa84c38e3e854fb605559f0a40008fa684 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:52:49 +0200 Subject: [PATCH 140/412] fix some typos --- README.md | 4 +- qurator/eynollah/eynollah.py | 52 ++++++------ qurator/eynollah/utils/__init__.py | 24 +++--- qurator/eynollah/utils/counter.py | 8 +- qurator/eynollah/utils/drop_capitals.py | 104 ++++++++++++------------ qurator/eynollah/writer.py | 56 ++++++------- 6 files changed, 124 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index d74c4a9..f6bc794 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ The following options can be used to further configure the processing: | `-tab` | apply table detection | | `-ae` | apply enhancement (the resulting image is saved to the output directory) | | `-as` | apply scaling | -| `-cl` | apply countour detection for curved text lines instead of bounding boxes | +| `-cl` | apply contour detection for curved text lines instead of bounding boxes | | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | | `-ho` | ignore headers for reading order dectection | @@ -99,7 +99,7 @@ uses the original (RGB) image despite any binarization that may have occured in If you find this tool useful in your work, please consider citing our paper: ```bibtex -@inproceedings{rezanezhad2023documentlayoutanalysis, +@inproceedings{rezanezhad2023eynollah, title = {Document Layout Analysis with Deep Learning and Heuristics}, author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ad3f312..625f39a 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1159,7 +1159,7 @@ class Eynollah: processes[i].start() slopes = [] - all_found_texline_polygons = [] + all_found_textline_polygons = [] all_found_text_regions = [] all_found_text_regions_par = [] boxes = [] @@ -1176,7 +1176,7 @@ class Eynollah: indexes_for_subprocess = list_all_par[6] for j in range(len(slopes_for_sub_process)): slopes.append(slopes_for_sub_process[j]) - all_found_texline_polygons.append(polys_for_sub_process[j]) + all_found_textline_polygons.append(polys_for_sub_process[j]) boxes.append(boxes_for_sub_process[j]) all_found_text_regions.append(contours_for_subprocess[j]) all_found_text_regions_par.append(contours_par_for_subprocess[j]) @@ -1186,7 +1186,7 @@ class Eynollah: processes[i].join() self.logger.debug('slopes %s', slopes) self.logger.debug("exit get_slopes_and_deskew_new") - return slopes, all_found_texline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con + return slopes, all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con def get_slopes_and_deskew_new(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): self.logger.debug("enter get_slopes_and_deskew_new") @@ -1207,7 +1207,7 @@ class Eynollah: processes[i].start() slopes = [] - all_found_texline_polygons = [] + all_found_textline_polygons = [] all_found_text_regions = [] all_found_text_regions_par = [] boxes = [] @@ -1224,7 +1224,7 @@ class Eynollah: indexes_for_subprocess = list_all_par[6] for j in range(len(slopes_for_sub_process)): slopes.append(slopes_for_sub_process[j]) - all_found_texline_polygons.append(polys_for_sub_process[j]) + all_found_textline_polygons.append(polys_for_sub_process[j]) boxes.append(boxes_for_sub_process[j]) all_found_text_regions.append(contours_for_subprocess[j]) all_found_text_regions_par.append(contours_par_for_subprocess[j]) @@ -1234,7 +1234,7 @@ class Eynollah: processes[i].join() self.logger.debug('slopes %s', slopes) self.logger.debug("exit get_slopes_and_deskew_new") - return slopes, all_found_texline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con + return slopes, all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con def get_slopes_and_deskew_new_curved(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, mask_texts_only, num_col, scale_par, slope_deskew): self.logger.debug("enter get_slopes_and_deskew_new_curved") @@ -1257,7 +1257,7 @@ class Eynollah: processes[i].start() slopes = [] - all_found_texline_polygons = [] + all_found_textline_polygons = [] all_found_text_regions = [] all_found_text_regions_par = [] boxes = [] @@ -1275,7 +1275,7 @@ class Eynollah: slopes_for_sub_process = list_all_par[6] for j in range(len(polys_for_sub_process)): slopes.append(slopes_for_sub_process[j]) - all_found_texline_polygons.append(polys_for_sub_process[j][::-1]) + all_found_textline_polygons.append(polys_for_sub_process[j][::-1]) boxes.append(boxes_for_sub_process[j]) all_found_text_regions.append(contours_for_subprocess[j]) all_found_text_regions_par.append(contours_par_for_subprocess[j]) @@ -1285,7 +1285,7 @@ class Eynollah: for i in range(num_cores): processes[i].join() # print(slopes,'slopes') - return all_found_texline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con, slopes + return all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con, slopes def do_work_of_slopes_new_curved(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, image_page_rotated, mask_texts_only, num_col, scale_par, indexes_r_con_per_pro, slope_deskew): self.logger.debug("enter do_work_of_slopes_new_curved") @@ -3007,37 +3007,37 @@ class Eynollah: if not self.curved_line: if self.light_version: if self.textline_light: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) else: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: - slopes, all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: scale_param = 1 - all_found_texline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_texline_polygons = small_textlines_to_parent_adherence2(all_found_texline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_texline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_texline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_texline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: #takes long timee contours_only_text_parent_d_ordered = None if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_texline_polygons, slopes, contours_only_text_parent_d_ordered) + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) if self.plotter: self.plotter.save_plot_of_layout(text_regions_p, image_page) @@ -3045,7 +3045,7 @@ class Eynollah: pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_texline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_texline_polygons, all_found_texline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) pixel_lines = 6 @@ -3091,7 +3091,7 @@ class Eynollah: else: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts else: @@ -3101,7 +3101,7 @@ class Eynollah: else: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_texline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts self.writer.write_pagexml(pcgts) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index b85abdf..d2b2488 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -796,7 +796,7 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch): return layout_in_patch -def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_texline_polygons,slopes,contours_only_text_parent_d_ordered): +def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_textline_polygons,slopes,contours_only_text_parent_d_ordered): cx_main,cy_main ,x_min_main , x_max_main, y_min_main ,y_max_main,y_corr_x_min_from_argmin=find_new_features_of_contours(contours_only_text_parent) @@ -805,8 +805,8 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions - all_found_texline_polygons_main=[] - all_found_texline_polygons_head=[] + all_found_textline_polygons_main=[] + all_found_textline_polygons_head=[] all_box_coord_main=[] all_box_coord_head=[] @@ -840,7 +840,7 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions contours_only_text_parent_head_d.append(contours_only_text_parent_d_ordered[ii]) all_box_coord_head.append(all_box_coord[ii]) slopes_head.append(slopes[ii]) - all_found_texline_polygons_head.append(all_found_texline_polygons[ii]) + all_found_textline_polygons_head.append(all_found_textline_polygons[ii]) else: regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=1 contours_only_text_parent_main.append(con) @@ -848,14 +848,14 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions contours_only_text_parent_main_d.append(contours_only_text_parent_d_ordered[ii]) all_box_coord_main.append(all_box_coord[ii]) slopes_main.append(slopes[ii]) - all_found_texline_polygons_main.append(all_found_texline_polygons[ii]) + all_found_textline_polygons_main.append(all_found_textline_polygons[ii]) #print(all_pixels,pixels_main,pixels_header) - return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_texline_polygons_main,all_found_texline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d + return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_textline_polygons_main,all_found_textline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d -def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_texline_polygons,slopes,contours_only_text_parent_d_ordered): +def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_textline_polygons,slopes,contours_only_text_parent_d_ordered): ### to make it faster h_o = regions_model_1.shape[0] @@ -874,8 +874,8 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r - all_found_texline_polygons_main=[] - all_found_texline_polygons_head=[] + all_found_textline_polygons_main=[] + all_found_textline_polygons_head=[] all_box_coord_main=[] all_box_coord_head=[] @@ -909,7 +909,7 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r contours_only_text_parent_head_d.append(contours_only_text_parent_d_ordered[ii]) all_box_coord_head.append(all_box_coord[ii]) slopes_head.append(slopes[ii]) - all_found_texline_polygons_head.append(all_found_texline_polygons[ii]) + all_found_textline_polygons_head.append(all_found_textline_polygons[ii]) else: regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=1 contours_only_text_parent_main.append(con) @@ -917,7 +917,7 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r contours_only_text_parent_main_d.append(contours_only_text_parent_d_ordered[ii]) all_box_coord_main.append(all_box_coord[ii]) slopes_main.append(slopes[ii]) - all_found_texline_polygons_main.append(all_found_texline_polygons[ii]) + all_found_textline_polygons_main.append(all_found_textline_polygons[ii]) #print(all_pixels,pixels_main,pixels_header) @@ -931,7 +931,7 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r contours_only_text_parent_main = [ (i*3.).astype(np.int32) for i in contours_only_text_parent_main] ### - return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_texline_polygons_main,all_found_texline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d + return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_textline_polygons_main,all_found_textline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col): # print(textlines_con) diff --git a/qurator/eynollah/utils/counter.py b/qurator/eynollah/utils/counter.py index bc1d765..9a3ed70 100644 --- a/qurator/eynollah/utils/counter.py +++ b/qurator/eynollah/utils/counter.py @@ -7,13 +7,13 @@ class EynollahIdCounter(): def __init__(self, region_idx=0, line_idx=0): self._counter = Counter() - self._inital_region_idx = region_idx - self._inital_line_idx = line_idx + self._initial_region_idx = region_idx + self._initial_line_idx = line_idx self.reset() def reset(self): - self.set('region', self._inital_region_idx) - self.set('line', self._inital_line_idx) + self.set('region', self._initial_region_idx) + self.set('line', self._initial_line_idx) def inc(self, name, val=1): self._counter.update({name: val}) diff --git a/qurator/eynollah/utils/drop_capitals.py b/qurator/eynollah/utils/drop_capitals.py index 6d1edfa..e12028f 100644 --- a/qurator/eynollah/utils/drop_capitals.py +++ b/qurator/eynollah/utils/drop_capitals.py @@ -13,13 +13,13 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_only_text_parent_h, all_box_coord, all_box_coord_h, - all_found_texline_polygons, - all_found_texline_polygons_h, + all_found_textline_polygons, + all_found_textline_polygons_h, kernel=None, curved_line=False, ): - # print(np.shape(all_found_texline_polygons),np.shape(all_found_texline_polygons[3]),'all_found_texline_polygonsshape') - # print(all_found_texline_polygons[3]) + # print(np.shape(all_found_textline_polygons),np.shape(all_found_textline_polygons[3]),'all_found_textline_polygonsshape') + # print(all_found_textline_polygons[3]) cx_m, cy_m, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) cx_h, cy_h, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_h) cx_d, cy_d, _, _, y_min_d, y_max_d, _ = find_new_features_of_contours(polygons_of_drop_capitals) @@ -87,9 +87,9 @@ def adhere_drop_capital_region_into_corresponding_textline( region_final = region_with_intersected_drop[np.argmax(sum_pixels_of_intersection)] - 1 # print(region_final,'region_final') - # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) try: - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(all_box_coord[j_cont]) # print(cx_t) # print(cy_t) @@ -105,9 +105,9 @@ def adhere_drop_capital_region_into_corresponding_textline( arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) # print(arg_min) - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] - cnt_nearest[:, 0, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] + cnt_nearest = np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0, 0] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] + cnt_nearest[:, 0, 1] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) @@ -131,7 +131,7 @@ def adhere_drop_capital_region_into_corresponding_textline( # contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest except: # print('gordun1') @@ -139,11 +139,11 @@ def adhere_drop_capital_region_into_corresponding_textline( elif len(region_with_intersected_drop) == 1: region_final = region_with_intersected_drop[0] - 1 - # areas_main=np.array([cv2.contourArea(all_found_texline_polygons[int(region_final)][0][j] ) for j in range(len(all_found_texline_polygons[int(region_final)]))]) + # areas_main=np.array([cv2.contourArea(all_found_textline_polygons[int(region_final)][0][j] ) for j in range(len(all_found_textline_polygons[int(region_final)]))]) - # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) try: - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(all_box_coord[j_cont]) # print(cx_t) # print(cy_t) @@ -157,9 +157,9 @@ def adhere_drop_capital_region_into_corresponding_textline( arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) # print(arg_min) - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] - cnt_nearest[:, 0, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] + cnt_nearest = np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0, 0] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] + cnt_nearest[:, 0, 1] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) @@ -184,15 +184,15 @@ def adhere_drop_capital_region_into_corresponding_textline( # contours_biggest[:,0,0]=contours_biggest[:,0,0]#-all_box_coord[int(region_final)][2] # contours_biggest[:,0,1]=contours_biggest[:,0,1]#-all_box_coord[int(region_final)][0] # print(np.shape(contours_biggest),'contours_biggest') - # print(np.shape(all_found_texline_polygons[int(region_final)][arg_min])) + # print(np.shape(all_found_textline_polygons[int(region_final)][arg_min])) ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest except: pass try: - # print(all_found_texline_polygons[j_cont][0]) - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # print(all_found_textline_polygons[j_cont][0]) + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(all_box_coord[j_cont]) # print(cx_t) # print(cy_t) @@ -206,9 +206,9 @@ def adhere_drop_capital_region_into_corresponding_textline( arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) # print(arg_min) - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] - cnt_nearest[:, 0, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] + cnt_nearest = np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0, 0] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 0] # +all_box_coord[int(region_final)][2] + cnt_nearest[:, 0, 1] = all_found_textline_polygons[int(region_final)][arg_min][:, 0, 1] # +all_box_coord[int(region_final)][0] img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) @@ -231,15 +231,15 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest[:, 0, 1] = contours_biggest[:, 0, 1] # -all_box_coord[int(region_final)][0] ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest - # all_found_texline_polygons[int(region_final)][arg_min]=contours_biggest + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + # all_found_textline_polygons[int(region_final)][arg_min]=contours_biggest except: pass else: pass - ##cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + ##cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) ###print(all_box_coord[j_cont]) ###print(cx_t) ###print(cy_t) @@ -253,9 +253,9 @@ def adhere_drop_capital_region_into_corresponding_textline( ##arg_min=np.argmin(np.abs(y_lines-y_min_d[i_drop]) ) ###print(arg_min) - ##cnt_nearest=np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - ##cnt_nearest[:,0,0]=all_found_texline_polygons[int(region_final)][arg_min][:,0,0]#+all_box_coord[int(region_final)][2] - ##cnt_nearest[:,0,1]=all_found_texline_polygons[int(region_final)][arg_min][:,0,1]#+all_box_coord[int(region_final)][0] + ##cnt_nearest=np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + ##cnt_nearest[:,0,0]=all_found_textline_polygons[int(region_final)][arg_min][:,0,0]#+all_box_coord[int(region_final)][2] + ##cnt_nearest[:,0,1]=all_found_textline_polygons[int(region_final)][arg_min][:,0,1]#+all_box_coord[int(region_final)][0] ##img_textlines=np.zeros((text_regions_p.shape[0],text_regions_p.shape[1],3)) ##img_textlines=cv2.fillPoly(img_textlines,pts=[cnt_nearest],color=(255,255,255)) @@ -281,7 +281,7 @@ def adhere_drop_capital_region_into_corresponding_textline( ##contours_biggest[:,0,1]=contours_biggest[:,0,1]#-all_box_coord[int(region_final)][0] ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - ##all_found_texline_polygons[int(region_final)][arg_min]=contours_biggest + ##all_found_textline_polygons[int(region_final)][arg_min]=contours_biggest else: if len(region_with_intersected_drop) > 1: @@ -293,9 +293,9 @@ def adhere_drop_capital_region_into_corresponding_textline( region_final = region_with_intersected_drop[np.argmax(sum_pixels_of_intersection)] - 1 # print(region_final,'region_final') - # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) try: - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(all_box_coord[j_cont]) # print(cx_t) # print(cy_t) @@ -311,9 +311,9 @@ def adhere_drop_capital_region_into_corresponding_textline( arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) # print(arg_min) - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0] + all_box_coord[int(region_final)][2] - cnt_nearest[:, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 1] + all_box_coord[int(region_final)][0] + cnt_nearest = np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0] = all_found_textline_polygons[int(region_final)][arg_min][:, 0] + all_box_coord[int(region_final)][2] + cnt_nearest[:, 1] = all_found_textline_polygons[int(region_final)][arg_min][:, 1] + all_box_coord[int(region_final)][0] img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) @@ -337,7 +337,7 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest = contours_biggest.reshape(np.shape(contours_biggest)[0], np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest except: # print('gordun1') @@ -345,14 +345,14 @@ def adhere_drop_capital_region_into_corresponding_textline( elif len(region_with_intersected_drop) == 1: region_final = region_with_intersected_drop[0] - 1 - # areas_main=np.array([cv2.contourArea(all_found_texline_polygons[int(region_final)][0][j] ) for j in range(len(all_found_texline_polygons[int(region_final)]))]) + # areas_main=np.array([cv2.contourArea(all_found_textline_polygons[int(region_final)][0][j] ) for j in range(len(all_found_textline_polygons[int(region_final)]))]) - # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(cx_t,'print') try: - # print(all_found_texline_polygons[j_cont][0]) - cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_texline_polygons[int(region_final)]) + # print(all_found_textline_polygons[j_cont][0]) + cx_t, cy_t, _, _, _, _, _ = find_new_features_of_contours(all_found_textline_polygons[int(region_final)]) # print(all_box_coord[j_cont]) # print(cx_t) # print(cy_t) @@ -366,9 +366,9 @@ def adhere_drop_capital_region_into_corresponding_textline( arg_min = np.argmin(np.abs(y_lines - y_min_d[i_drop])) # print(arg_min) - cnt_nearest = np.copy(all_found_texline_polygons[int(region_final)][arg_min]) - cnt_nearest[:, 0] = all_found_texline_polygons[int(region_final)][arg_min][:, 0] + all_box_coord[int(region_final)][2] - cnt_nearest[:, 1] = all_found_texline_polygons[int(region_final)][arg_min][:, 1] + all_box_coord[int(region_final)][0] + cnt_nearest = np.copy(all_found_textline_polygons[int(region_final)][arg_min]) + cnt_nearest[:, 0] = all_found_textline_polygons[int(region_final)][arg_min][:, 0] + all_box_coord[int(region_final)][2] + cnt_nearest[:, 1] = all_found_textline_polygons[int(region_final)][arg_min][:, 1] + all_box_coord[int(region_final)][0] img_textlines = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1], 3)) img_textlines = cv2.fillPoly(img_textlines, pts=[cnt_nearest], color=(255, 255, 255)) @@ -391,8 +391,8 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest[:, 0, 1] = contours_biggest[:, 0, 1] - all_box_coord[int(region_final)][0] contours_biggest = contours_biggest.reshape(np.shape(contours_biggest)[0], np.shape(contours_biggest)[2]) - all_found_texline_polygons[int(region_final)][arg_min] = contours_biggest - # all_found_texline_polygons[int(region_final)][arg_min]=contours_biggest + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + # all_found_textline_polygons[int(region_final)][arg_min]=contours_biggest except: pass @@ -417,8 +417,8 @@ def adhere_drop_capital_region_into_corresponding_textline( ######plt.show() #####try: #####if len(contours_new_parent)==1: - ######print(all_found_texline_polygons[j_cont][0]) - #####cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_texline_polygons[j_cont]) + ######print(all_found_textline_polygons[j_cont][0]) + #####cx_t,cy_t ,_, _, _ ,_,_= find_new_features_of_contours(all_found_textline_polygons[j_cont]) ######print(all_box_coord[j_cont]) ######print(cx_t) ######print(cy_t) @@ -431,9 +431,9 @@ def adhere_drop_capital_region_into_corresponding_textline( #####arg_min=np.argmin(np.abs(y_lines-y_min_d[i_drop]) ) ######print(arg_min) - #####cnt_nearest=np.copy(all_found_texline_polygons[j_cont][arg_min]) - #####cnt_nearest[:,0]=all_found_texline_polygons[j_cont][arg_min][:,0]+all_box_coord[j_cont][2] - #####cnt_nearest[:,1]=all_found_texline_polygons[j_cont][arg_min][:,1]+all_box_coord[j_cont][0] + #####cnt_nearest=np.copy(all_found_textline_polygons[j_cont][arg_min]) + #####cnt_nearest[:,0]=all_found_textline_polygons[j_cont][arg_min][:,0]+all_box_coord[j_cont][2] + #####cnt_nearest[:,1]=all_found_textline_polygons[j_cont][arg_min][:,1]+all_box_coord[j_cont][0] #####img_textlines=np.zeros((text_regions_p.shape[0],text_regions_p.shape[1],3)) #####img_textlines=cv2.fillPoly(img_textlines,pts=[cnt_nearest],color=(255,255,255)) @@ -454,7 +454,7 @@ def adhere_drop_capital_region_into_corresponding_textline( #####contours_biggest[:,0,0]=contours_biggest[:,0,0]-all_box_coord[j_cont][2] #####contours_biggest[:,0,1]=contours_biggest[:,0,1]-all_box_coord[j_cont][0] - #####all_found_texline_polygons[j_cont][arg_min]=contours_biggest + #####all_found_textline_polygons[j_cont][arg_min]=contours_biggest ######print(contours_biggest) ######plt.imshow(img_textlines[:,:,0]) ######plt.show() @@ -462,7 +462,7 @@ def adhere_drop_capital_region_into_corresponding_textline( #####pass #####except: #####pass - return all_found_texline_polygons + return all_found_textline_polygons def filter_small_drop_capitals_from_no_patch_layout(layout_no_patch, layout1): diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index d5704f6..f537f65 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -54,54 +54,54 @@ class EynollahXmlWriter(): points_page_print = points_page_print + ' ' return points_page_print[:-1] - def serialize_lines_in_marginal(self, marginal_region, all_found_texline_polygons_marginals, marginal_idx, page_coord, all_box_coord_marginals, slopes_marginals, counter): - for j in range(len(all_found_texline_polygons_marginals[marginal_idx])): + def serialize_lines_in_marginal(self, marginal_region, all_found_textline_polygons_marginals, marginal_idx, page_coord, all_box_coord_marginals, slopes_marginals, counter): + for j in range(len(all_found_textline_polygons_marginals[marginal_idx])): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) marginal_region.add_TextLine(textline) points_co = '' - for l in range(len(all_found_texline_polygons_marginals[marginal_idx][j])): + for l in range(len(all_found_textline_polygons_marginals[marginal_idx][j])): if not (self.curved_line or self.textline_light): - if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: - textline_x_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x) ) - textline_y_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y) ) + if len(all_found_textline_polygons_marginals[marginal_idx][j][l]) == 2: + textline_x_coord = max(0, int((all_found_textline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x) ) + textline_y_coord = max(0, int((all_found_textline_polygons_marginals[marginal_idx][j][l][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y) ) else: - textline_x_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x) ) - textline_y_coord = max(0, int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y) ) + textline_x_coord = max(0, int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x) ) + textline_y_coord = max(0, int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y) ) points_co += str(textline_x_coord) points_co += ',' points_co += str(textline_y_coord) if (self.curved_line or self.textline_light) and np.abs(slopes_marginals[marginal_idx]) <= 45: - if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + page_coord[2]) / self.scale_x)) + if len(all_found_textline_polygons_marginals[marginal_idx][j][l]) == 2: + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0] + page_coord[2]) / self.scale_x)) points_co += ',' - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][1] + page_coord[0]) / self.scale_y)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][1] + page_coord[0]) / self.scale_y)) else: - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][0] + page_coord[2]) / self.scale_x)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][0] + page_coord[2]) / self.scale_x)) points_co += ',' - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][1] + page_coord[0]) / self.scale_y)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][1] + page_coord[0]) / self.scale_y)) elif (self.curved_line or self.textline_light) and np.abs(slopes_marginals[marginal_idx]) > 45: - if len(all_found_texline_polygons_marginals[marginal_idx][j][l]) == 2: - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x)) + if len(all_found_textline_polygons_marginals[marginal_idx][j][l]) == 2: + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x)) points_co += ',' - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y)) else: - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][0] + all_box_coord_marginals[marginal_idx][2] + page_coord[2]) / self.scale_x)) points_co += ',' - points_co += str(int((all_found_texline_polygons_marginals[marginal_idx][j][l][0][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y)) + points_co += str(int((all_found_textline_polygons_marginals[marginal_idx][j][l][0][1] + all_box_coord_marginals[marginal_idx][0] + page_coord[0]) / self.scale_y)) points_co += ' ' coords.set_points(points_co[:-1]) - def serialize_lines_in_region(self, text_region, all_found_texline_polygons, region_idx, page_coord, all_box_coord, slopes, counter): + def serialize_lines_in_region(self, text_region, all_found_textline_polygons, region_idx, page_coord, all_box_coord, slopes, counter): self.logger.debug('enter serialize_lines_in_region') - for j in range(len(all_found_texline_polygons[region_idx])): + for j in range(len(all_found_textline_polygons[region_idx])): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) text_region.add_TextLine(textline) region_bboxes = all_box_coord[region_idx] points_co = '' - for idx_contour_textline, contour_textline in enumerate(all_found_texline_polygons[region_idx][j]): + for idx_contour_textline, contour_textline in enumerate(all_found_textline_polygons[region_idx][j]): if not (self.curved_line or self.textline_light): if len(contour_textline) == 2: textline_x_coord = max(0, int((contour_textline[0] + region_bboxes[2] + page_coord[2]) / self.scale_x)) @@ -140,7 +140,7 @@ class EynollahXmlWriter(): with open(out_fname, 'w') as f: f.write(to_xml(pcgts)) - def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_texline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables): + def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables): self.logger.debug('enter build_pagexml_no_full_layout') # create the file structure @@ -159,13 +159,13 @@ class EynollahXmlWriter(): Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord)), ) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_texline_polygons, mm, page_coord, all_box_coord, slopes, counter) + self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter) for mm in range(len(found_polygons_marginals)): marginal = TextRegionType(id=counter.next_region_id, type_='marginalia', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_marginals[mm], page_coord))) page.add_TextRegion(marginal) - self.serialize_lines_in_marginal(marginal, all_found_texline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) + self.serialize_lines_in_marginal(marginal, all_found_textline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) for mm in range(len(found_polygons_text_region_img)): img_region = ImageRegionType(id=counter.next_region_id, Coords=CoordsType()) @@ -201,7 +201,7 @@ class EynollahXmlWriter(): return pcgts - def build_pagexml_full_layout(self, found_polygons_text_region, found_polygons_text_region_h, page_coord, order_of_texts, id_of_texts, all_found_texline_polygons, all_found_texline_polygons_h, all_box_coord, all_box_coord_h, found_polygons_text_region_img, found_polygons_tables, found_polygons_drop_capitals, found_polygons_marginals, all_found_texline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml): + def build_pagexml_full_layout(self, found_polygons_text_region, found_polygons_text_region_h, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, found_polygons_text_region_img, found_polygons_tables, found_polygons_drop_capitals, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml): self.logger.debug('enter build_pagexml_full_layout') # create the file structure @@ -218,20 +218,20 @@ class EynollahXmlWriter(): textregion = TextRegionType(id=counter.next_region_id, type_='paragraph', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord))) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_texline_polygons, mm, page_coord, all_box_coord, slopes, counter) + self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter) self.logger.debug('len(found_polygons_text_region_h) %s', len(found_polygons_text_region_h)) for mm in range(len(found_polygons_text_region_h)): textregion = TextRegionType(id=counter.next_region_id, type_='header', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region_h[mm], page_coord))) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_texline_polygons_h, mm, page_coord, all_box_coord_h, slopes_h, counter) + self.serialize_lines_in_region(textregion, all_found_textline_polygons_h, mm, page_coord, all_box_coord_h, slopes_h, counter) for mm in range(len(found_polygons_marginals)): marginal = TextRegionType(id=counter.next_region_id, type_='marginalia', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_marginals[mm], page_coord))) page.add_TextRegion(marginal) - self.serialize_lines_in_marginal(marginal, all_found_texline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) + self.serialize_lines_in_marginal(marginal, all_found_textline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) for mm in range(len(found_polygons_drop_capitals)): page.add_TextRegion(TextRegionType(id=counter.next_region_id, type_='drop-capital', From 7a70b20c77ad5539e9b157574d534ade55bf4f62 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 5 Sep 2023 13:43:11 +0200 Subject: [PATCH 141/412] apply missed commit #a56988a back --- qurator/eynollah/eynollah.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 3424180..4b1b5e9 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2909,7 +2909,7 @@ class Eynollah: contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -2924,7 +2924,7 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d)[index_con_parents_d]) + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) @@ -2987,7 +2987,7 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent)[index_con_parents]) + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -3026,7 +3026,7 @@ class Eynollah: if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) if self.light_version: text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: @@ -3099,7 +3099,7 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) From 03bfd7a3906cb956879195ce93e61a6eee59fd86 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:16:20 +0200 Subject: [PATCH 142/412] Update requirements.txt Update to `tensorflow>=2.12` (drops Python 3.7 support) * fix #114 * fix #115 Tested by @vahidrezanezhad @cneud --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a19a191..530dac2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 -tensorflow >= 2.4.0, <2.12.0 +tensorflow >=2.12.0 imutils >= 0.5.3 matplotlib setuptools >= 50 From 9d3a1a5b769e6a34b1e26255f999ac4b45f72b94 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:32:20 +0200 Subject: [PATCH 143/412] Update test-eynollah.yml --- .github/workflows/test-eynollah.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index e06cb35..d380408 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -3,7 +3,7 @@ name: Python package -on: [push, pull_request] +on: [push] jobs: build: @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8'] + python-version: ['3.8', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 @@ -33,4 +33,4 @@ jobs: pip install . pip install -r requirements-test.txt - name: Test with pytest - run: make test \ No newline at end of file + run: make test From 6c65fc4dfe54f0471d0178640a68581e37d9ae56 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:33:05 +0200 Subject: [PATCH 144/412] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a782d8f..d2b7057 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,5 +47,5 @@ workflows: version: 2 build: jobs: - - build-python37 + # - build-python37 - build-python38 From 56934c876a796cfe45bcbc1f0ce1e070234c3de7 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:53:10 +0200 Subject: [PATCH 145/412] remove duplicate test for Python 3.8 --- .github/workflows/test-eynollah.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index d380408..30c9729 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 From 4254ce3bdbabaf34bcf79d06df6acc6a1c128527 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:54:14 +0200 Subject: [PATCH 146/412] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 003cc2d..47d81bc 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface ## Installation -Python versions `3.7-3.10` with Tensorflow versions `2.4-2.11` are currently supported. +Python versions `3.8-3.11` with Tensorflow versions >=`2.12` are currently supported. -For (limited) GPU support the [matching](https://www.tensorflow.org/install/source#gpu) CUDA toolkit needs to be installed. +For (limited) GPU support the CUDA toolkit needs to be installed. You can either install via From 5fdc6d4fa48d25309d1a774b04e79debbf797e75 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 14 Oct 2023 09:05:05 +0200 Subject: [PATCH 147/412] integration of machine based reading order detection --- qurator/eynollah/eynollah.py | 222 +++++++++++++++++++++++++++--- qurator/eynollah/utils/contour.py | 11 +- 2 files changed, 209 insertions(+), 24 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 4b1b5e9..b83db98 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -78,6 +78,7 @@ from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter from .writer import EynollahXmlWriter +MIN_AREA_REGION = 0.0005 SLOPE_THRESHOLD = 0.13 RATIO_OF_TWO_MODEL_THRESHOLD = 95.50 #98.45: DPI_THRESHOLD = 298 @@ -225,6 +226,7 @@ class Eynollah: self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" + self.model_reading_order_machine_dir = dir_models + "/model_6_reading_order_machine_based" if self.textline_light: self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: @@ -246,6 +248,7 @@ class Eynollah: self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) self.ls_imgs = os.listdir(self.dir_in) @@ -264,6 +267,7 @@ class Eynollah: self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) + self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) self.ls_imgs = os.listdir(self.dir_in) @@ -1647,9 +1651,39 @@ class Eynollah: mask_images_only=(prediction_regions_org[:,:] ==2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + test_khat = np.zeros(prediction_regions_org.shape) + + test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) + + + #plt.imshow(test_khat[:,:]) + #plt.show() + + #for jv in range(1): + #print(jv, hir_lines_xml[0][232][3]) + #test_khat = np.zeros(prediction_regions_org.shape) + + #test_khat = cv2.fillPoly(test_khat, pts = [polygons_lines_xml[232]], color=(1,1,1)) + + + #plt.imshow(test_khat[:,:]) + #plt.show() + + + polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + + + test_khat = np.zeros(prediction_regions_org.shape) + + test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) + + + #plt.imshow(test_khat[:,:]) + #plt.show() + #sys.exit() + polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) @@ -1785,7 +1819,7 @@ class Eynollah: polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only, 1, 0.00001) polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only, 1, 0.00001) @@ -1853,7 +1887,7 @@ class Eynollah: mask_images_only=(prediction_regions_org[:,:] ==2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) @@ -2821,13 +2855,157 @@ class Eynollah: model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model + + def do_order_of_regions_with_machine(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): + + #print(text_regions_p.shape) + y_len = text_regions_p.shape[0] + x_len = text_regions_p.shape[1] + + img_poly = np.zeros((y_len,x_len), dtype='uint8') + + unique_pix = np.unique(text_regions_p) + #print(unique_pix, 'unique_pix') + + #for pix in unique_pix: + #print(pix) + #plt.imshow((text_regions_p[:,:]==pix)*1 ) + #plt.show() + + img_poly[text_regions_p[:,:]==1] = 1 + img_poly[text_regions_p[:,:]==2] = 2 + img_poly[text_regions_p[:,:]==3] = 4 + img_poly[text_regions_p[:,:]==6] = 5 + + #plt.imshow(text_regions_p) + #plt.show() + + + #plt.imshow(img_poly) + #plt.show() + model_ro_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) + + height1 =672#448 + width1 = 448#224 + + height2 =672#448 + width2= 448#224 + + height3 =672#448 + width3 = 448#224 + + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + + + img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') + + for j in range(len(cy_main)): + #print(j, int(y_max_main[j]), x_min_main[j], x_max_main[j] ) + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 + + #plt.imshow(img_header_and_sep[:,:]) + #plt.show() + + co_text_all = contours_only_text_parent + contours_only_text_parent_h + #id_all_text = id_paragraph + id_header + + #texts_corr_order_index = [index_tot_regions[tot_region_ref.index(i)] for i in id_all_text ] + #texts_corr_order_index_int = [int(x) for x in texts_corr_order_index] + + #co_text_all, texts_corr_order_index_int = filter_contours_area_of_image(img_poly, co_text_all, texts_corr_order_index_int, max_area, min_area) + + labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') + for i in range(len(co_text_all)): + img_label = np.zeros((y_len,x_len,3),dtype='uint8') + img_label=cv2.fillPoly(img_label, pts =[co_text_all[i]], color=(1,1,1)) + labels_con[:,:,i] = img_label[:,:,0] + + + img3= np.copy(img_poly) + + labels_con = resize_image(labels_con, height1, width1) + + img_header_and_sep = resize_image(img_header_and_sep, height1, width1) + + img3= resize_image (img3, height3, width3) + + img3 = img3.astype(np.uint16) + + + #plt.imshow(img3) + #plt.show() + + order_matrix = np.zeros((labels_con.shape[2], labels_con.shape[2]))-1 + + for i in range(labels_con.shape[2]): + for j in range(labels_con.shape[2]): + if j>i: + img1= np.repeat(labels_con[:,:,i][:, :, np.newaxis], 3, axis=2) + img2 = np.repeat(labels_con[:,:,j][:, :, np.newaxis], 3, axis=2) + #img1 = img1.astype(np.uint16) + #img2 = img2.astype(np.uint16) + + img2[:,:,0][img3[:,:]==5] = 2 + img2[:,:,0][img_header_and_sep[:,:]==1] = 3 + + + + img1[:,:,0][img3[:,:]==5] = 2 + img1[:,:,0][img_header_and_sep[:,:]==1] = 3 + + + #plt.imshow(labels_con[:,:,i]) + #plt.show() + + #plt.imshow(img2[:,:,0]) + #plt.show() + + + #plt.imshow(img1[:,:,0]) + #plt.show() + + #sys.exit() + input_1= np.zeros( (height1, width1,3)) + + input_1[:,:,0] = img1[:,:,0]/3. + input_1[:,:,2] = img2[:,:,0]/3. + input_1[:,:,1] = img3[:,:]/5. + + #y_pr=model.predict([img1.reshape(1,height1,width1,3) , img2.reshape(1,height2,width2,3),img3.reshape(1,height3,width3,3) ], verbose=2) + y_pr=model_ro_machine.predict(input_1.reshape(1,height1,width1,3) , verbose=0) + #print(y_pr) + + if y_pr>=0.5: + order_class = 1 + else: + order_class = 0 + + order_matrix[i,j] = y_pr#order_class + order_matrix[j,i] = 1-y_pr#int( 1 - order_class) + + + sum_mat = np.sum(order_matrix, axis=1) + index_sort = np.argsort(sum_mat) + index_sort = index_sort[::-1] + + print(index_sort) + REGION_ID_TEMPLATE = 'region_%04d' + order_of_texts = [] + id_of_texts = [] + for order, id_text in enumerate(index_sort): + order_of_texts.append(id_text) + id_of_texts.append( REGION_ID_TEMPLATE % order ) + + + return order_of_texts, id_of_texts def run(self): """ Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - + + self.reading_order_machine_based = True#True t0_tot = time.time() @@ -2896,7 +3074,7 @@ class Eynollah: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - min_con_area = 0.000005 + ###min_con_area = 0.000005 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text, hir_on_text = return_contours_of_image(text_only) contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) @@ -2906,8 +3084,8 @@ class Eynollah: areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) #self.logger.info('areas_cnt_text %s', areas_cnt_text) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) @@ -2983,8 +3161,8 @@ class Eynollah: areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) @@ -3086,21 +3264,33 @@ class Eynollah: self.plotter.write_images_into_directory(polygons_of_images, image_page) t_order = time.time() if self.full_layout: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts + + print(id_of_texts_tot,'id_of_texts_tot') + print(order_text_new,'order_text_new') + else: contours_only_text_parent_h = None - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index bac8235..53b39b5 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -44,8 +44,8 @@ def get_text_region_boxes_by_given_contours(contours): def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area): found_polygons_early = list() - jv = 0 - for c in contours: + + for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue @@ -53,14 +53,12 @@ def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area area = polygon.area if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]) and hierarchy[0][jv][3] == -1: # and hierarchy[0][jv][3]==-1 : found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.uint)) - jv += 1 return found_polygons_early def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, min_area): found_polygons_early = list() - jv = 0 - for c in contours: + for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue @@ -73,7 +71,6 @@ def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, m if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]): # and hierarchy[0][jv][3]==-1 : # print(c[0][0][1]) found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.int32)) - jv += 1 return found_polygons_early def find_new_features_of_contours(contours_main): @@ -234,8 +231,6 @@ def get_textregion_contours_in_org_image_multi2(cnts, img, slope_first): with Pool(cpu_count()) as p: cnts_org = p.starmap(loop_contour_image, [(index_l,cnts, img,slope_first) for index_l in range(len(cnts))]) - print(len(cnts_org),'lendiha') - return cnts_org def get_textregion_contours_in_org_image(cnts, img, slope_first): From 7983a650065c392f64585e2ba540f639bde45bf6 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 14 Oct 2023 13:31:56 +0200 Subject: [PATCH 148/412] filtering separators in a correct way without missing them --- qurator/eynollah/utils/contour.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index bac8235..53b39b5 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -44,8 +44,8 @@ def get_text_region_boxes_by_given_contours(contours): def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area): found_polygons_early = list() - jv = 0 - for c in contours: + + for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue @@ -53,14 +53,12 @@ def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area area = polygon.area if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]) and hierarchy[0][jv][3] == -1: # and hierarchy[0][jv][3]==-1 : found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.uint)) - jv += 1 return found_polygons_early def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, min_area): found_polygons_early = list() - jv = 0 - for c in contours: + for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue @@ -73,7 +71,6 @@ def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, m if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]): # and hierarchy[0][jv][3]==-1 : # print(c[0][0][1]) found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.int32)) - jv += 1 return found_polygons_early def find_new_features_of_contours(contours_main): @@ -234,8 +231,6 @@ def get_textregion_contours_in_org_image_multi2(cnts, img, slope_first): with Pool(cpu_count()) as p: cnts_org = p.starmap(loop_contour_image, [(index_l,cnts, img,slope_first) for index_l in range(len(cnts))]) - print(len(cnts_org),'lendiha') - return cnts_org def get_textregion_contours_in_org_image(cnts, img, slope_first): From 49c93149a49b103b6434fee79ef28517fa4b13f9 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Oct 2023 10:01:28 +0200 Subject: [PATCH 149/412] machine based reading order inference with a variable batch size --- qurator/eynollah/eynollah.py | 102 +++++++++++++++-------------------- 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index b83db98..35992c9 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2857,32 +2857,20 @@ class Eynollah: return model def do_order_of_regions_with_machine(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): - - #print(text_regions_p.shape) y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] img_poly = np.zeros((y_len,x_len), dtype='uint8') unique_pix = np.unique(text_regions_p) - #print(unique_pix, 'unique_pix') - - #for pix in unique_pix: - #print(pix) - #plt.imshow((text_regions_p[:,:]==pix)*1 ) - #plt.show() + img_poly[text_regions_p[:,:]==1] = 1 img_poly[text_regions_p[:,:]==2] = 2 img_poly[text_regions_p[:,:]==3] = 4 img_poly[text_regions_p[:,:]==6] = 5 - #plt.imshow(text_regions_p) - #plt.show() - - - #plt.imshow(img_poly) - #plt.show() + model_ro_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) height1 =672#448 @@ -2900,19 +2888,11 @@ class Eynollah: img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') for j in range(len(cy_main)): - #print(j, int(y_max_main[j]), x_min_main[j], x_max_main[j] ) img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - #plt.imshow(img_header_and_sep[:,:]) - #plt.show() co_text_all = contours_only_text_parent + contours_only_text_parent_h - #id_all_text = id_paragraph + id_header - #texts_corr_order_index = [index_tot_regions[tot_region_ref.index(i)] for i in id_all_text ] - #texts_corr_order_index_int = [int(x) for x in texts_corr_order_index] - - #co_text_all, texts_corr_order_index_int = filter_contours_area_of_image(img_poly, co_text_all, texts_corr_order_index_int, max_area, min_area) labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') for i in range(len(co_text_all)): @@ -2932,63 +2912,69 @@ class Eynollah: img3 = img3.astype(np.uint16) - #plt.imshow(img3) - #plt.show() - order_matrix = np.zeros((labels_con.shape[2], labels_con.shape[2]))-1 + inference_bs = 6 + tot_counter = 1 + batch_counter = 0 + i_indexer = [] + j_indexer =[] + input_1= np.zeros( (inference_bs, height1, width1,3)) + + tot_iteration = int( ( labels_con.shape[2]*(labels_con.shape[2]-1) )/2. ) + full_bs_ite= tot_iteration//inference_bs + last_bs = tot_iteration % inference_bs + + #print(labels_con.shape[2],"number of regions for reading order") for i in range(labels_con.shape[2]): for j in range(labels_con.shape[2]): if j>i: img1= np.repeat(labels_con[:,:,i][:, :, np.newaxis], 3, axis=2) img2 = np.repeat(labels_con[:,:,j][:, :, np.newaxis], 3, axis=2) - #img1 = img1.astype(np.uint16) - #img2 = img2.astype(np.uint16) img2[:,:,0][img3[:,:]==5] = 2 img2[:,:,0][img_header_and_sep[:,:]==1] = 3 - - img1[:,:,0][img3[:,:]==5] = 2 img1[:,:,0][img_header_and_sep[:,:]==1] = 3 - #plt.imshow(labels_con[:,:,i]) - #plt.show() + i_indexer.append(i) + j_indexer.append(j) + + input_1[batch_counter,:,:,0] = img1[:,:,0]/3. + input_1[batch_counter,:,:,2] = img2[:,:,0]/3. + input_1[batch_counter,:,:,1] = img3[:,:]/5. + + batch_counter = batch_counter+1 + + if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): + y_pr=model_ro_machine.predict(input_1 , verbose=0) - #plt.imshow(img2[:,:,0]) - #plt.show() - - - #plt.imshow(img1[:,:,0]) - #plt.show() - - #sys.exit() - input_1= np.zeros( (height1, width1,3)) - - input_1[:,:,0] = img1[:,:,0]/3. - input_1[:,:,2] = img2[:,:,0]/3. - input_1[:,:,1] = img3[:,:]/5. - - #y_pr=model.predict([img1.reshape(1,height1,width1,3) , img2.reshape(1,height2,width2,3),img3.reshape(1,height3,width3,3) ], verbose=2) - y_pr=model_ro_machine.predict(input_1.reshape(1,height1,width1,3) , verbose=0) - #print(y_pr) - - if y_pr>=0.5: - order_class = 1 - else: - order_class = 0 + if batch_counter==inference_bs: + iteration_batches = inference_bs + else: + iteration_batches = last_bs + for jb in range(iteration_batches): + if y_pr[jb][0]>=0.5: + order_class = 1 + else: + order_class = 0 + + order_matrix[i_indexer[jb],j_indexer[jb]] = y_pr[jb][0]#order_class + order_matrix[j_indexer[jb],i_indexer[jb]] = 1-y_pr[jb][0]#int( 1 - order_class) - order_matrix[i,j] = y_pr#order_class - order_matrix[j,i] = 1-y_pr#int( 1 - order_class) + batch_counter = 0 + + i_indexer = [] + j_indexer = [] + tot_counter = tot_counter+1 sum_mat = np.sum(order_matrix, axis=1) index_sort = np.argsort(sum_mat) index_sort = index_sort[::-1] - print(index_sort) REGION_ID_TEMPLATE = 'region_%04d' order_of_texts = [] id_of_texts = [] @@ -3272,13 +3258,12 @@ class Eynollah: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts - print(id_of_texts_tot,'id_of_texts_tot') - print(order_text_new,'order_text_new') else: contours_only_text_parent_h = None @@ -3291,6 +3276,7 @@ class Eynollah: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts From 59c0d90e5af7ed3f1d3d8d7a78ecdcc17eb2fb59 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Oct 2023 10:17:46 +0200 Subject: [PATCH 150/412] machine based reading order inference & optimized algorithm --- qurator/eynollah/eynollah.py | 153 ++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 35992c9..63e71cb 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2855,7 +2855,6 @@ class Eynollah: model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model - def do_order_of_regions_with_machine(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] @@ -2983,6 +2982,154 @@ class Eynollah: id_of_texts.append( REGION_ID_TEMPLATE % order ) + return order_of_texts, id_of_texts + + def update_list_and_return_first_biger_than_one_length(self,index_element_to_be_updated, innner_index_pr_pos, pr_list, pos_list,list_inp): + list_inp.pop(index_element_to_be_updated) + if len(pr_list)>0: + list_inp.insert(index_element_to_be_updated, pr_list) + else: + index_element_to_be_updated = index_element_to_be_updated -1 + + list_inp.insert(index_element_to_be_updated+1, [innner_index_pr_pos]) + if len(pos_list)>0: + list_inp.insert(index_element_to_be_updated+2, pos_list) + + len_all_elements = [len(i) for i in list_inp] + list_len_bigger_1 = np.where(np.array(len_all_elements)>1) + list_len_bigger_1 = list_len_bigger_1[0] + + if len(list_len_bigger_1)>0: + early_list_bigger_than_one = list_len_bigger_1[0] + else: + early_list_bigger_than_one = -20 + return list_inp, early_list_bigger_than_one + def do_order_of_regions_with_machine_optimized_algorithm(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): + y_len = text_regions_p.shape[0] + x_len = text_regions_p.shape[1] + + img_poly = np.zeros((y_len,x_len), dtype='uint8') + + unique_pix = np.unique(text_regions_p) + + + img_poly[text_regions_p[:,:]==1] = 1 + img_poly[text_regions_p[:,:]==2] = 2 + img_poly[text_regions_p[:,:]==3] = 4 + img_poly[text_regions_p[:,:]==6] = 5 + + + model_ro_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) + + height1 =672#448 + width1 = 448#224 + + height2 =672#448 + width2= 448#224 + + height3 =672#448 + width3 = 448#224 + + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + + + img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') + + for j in range(len(cy_main)): + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 + + + co_text_all = contours_only_text_parent + contours_only_text_parent_h + + + labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') + for i in range(len(co_text_all)): + img_label = np.zeros((y_len,x_len,3),dtype='uint8') + img_label=cv2.fillPoly(img_label, pts =[co_text_all[i]], color=(1,1,1)) + labels_con[:,:,i] = img_label[:,:,0] + + + img3= np.copy(img_poly) + + labels_con = resize_image(labels_con, height1, width1) + + img_header_and_sep = resize_image(img_header_and_sep, height1, width1) + + img3= resize_image (img3, height3, width3) + + img3 = img3.astype(np.uint16) + + inference_bs = 4 + input_1= np.zeros( (inference_bs, height1, width1,3)) + starting_list_of_regions = [] + starting_list_of_regions.append( list(range(labels_con.shape[2])) ) + index_update = 0 + index_selected = starting_list_of_regions[0] + #print(labels_con.shape[2],"number of regions for reading order") + while index_update>=0: + ij_list = starting_list_of_regions[index_update] + i = ij_list[0] + ij_list.pop(0) + + pr_list = [] + post_list = [] + + batch_counter = 0 + tot_counter = 1 + + tot_iteration = len(ij_list) + full_bs_ite= tot_iteration//inference_bs + last_bs = tot_iteration % inference_bs + + jbatch_indexer =[] + for j in ij_list: + img1= np.repeat(labels_con[:,:,i][:, :, np.newaxis], 3, axis=2) + img2 = np.repeat(labels_con[:,:,j][:, :, np.newaxis], 3, axis=2) + + img2[:,:,0][img3[:,:]==5] = 2 + img2[:,:,0][img_header_and_sep[:,:]==1] = 3 + + img1[:,:,0][img3[:,:]==5] = 2 + img1[:,:,0][img_header_and_sep[:,:]==1] = 3 + + jbatch_indexer.append(j) + + input_1[batch_counter,:,:,0] = img1[:,:,0]/3. + input_1[batch_counter,:,:,2] = img2[:,:,0]/3. + input_1[batch_counter,:,:,1] = img3[:,:]/5. + + batch_counter = batch_counter+1 + + if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): + y_pr=model_ro_machine.predict(input_1 , verbose=0) + + if batch_counter==inference_bs: + iteration_batches = inference_bs + else: + iteration_batches = last_bs + for jb in range(iteration_batches): + if y_pr[jb][0]>=0.5: + post_list.append(jbatch_indexer[jb]) + else: + pr_list.append(jbatch_indexer[jb]) + + batch_counter = 0 + jbatch_indexer = [] + + tot_counter = tot_counter+1 + + starting_list_of_regions, index_update = self.update_list_and_return_first_biger_than_one_length(index_update, i, pr_list, post_list,starting_list_of_regions) + + index_sort = [i[0] for i in starting_list_of_regions ] + + REGION_ID_TEMPLATE = 'region_%04d' + order_of_texts = [] + id_of_texts = [] + for order, id_text in enumerate(index_sort): + order_of_texts.append(id_text) + id_of_texts.append( REGION_ID_TEMPLATE % order ) + + return order_of_texts, id_of_texts def run(self): @@ -3252,7 +3399,7 @@ class Eynollah: if self.full_layout: if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) @@ -3268,7 +3415,7 @@ class Eynollah: else: contours_only_text_parent_h = None if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) From 941d87328a45ad6df5df27c0a84a4b695de65c67 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Oct 2023 11:19:30 +0200 Subject: [PATCH 151/412] machine based reading order & works for not full layout case --- qurator/eynollah/eynollah.py | 86 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 63e71cb..c008476 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2881,16 +2881,17 @@ class Eynollah: height3 =672#448 width3 = 448#224 - _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) - - img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') - - for j in range(len(cy_main)): - img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - - co_text_all = contours_only_text_parent + contours_only_text_parent_h + if contours_only_text_parent_h: + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + + for j in range(len(cy_main)): + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 + + co_text_all = contours_only_text_parent + contours_only_text_parent_h + else: + co_text_all = contours_only_text_parent labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') @@ -2984,7 +2985,7 @@ class Eynollah: return order_of_texts, id_of_texts - def update_list_and_return_first_biger_than_one_length(self,index_element_to_be_updated, innner_index_pr_pos, pr_list, pos_list,list_inp): + def update_list_and_return_first_with_length_bigger_than_one(self,index_element_to_be_updated, innner_index_pr_pos, pr_list, pos_list,list_inp): list_inp.pop(index_element_to_be_updated) if len(pr_list)>0: list_inp.insert(index_element_to_be_updated, pr_list) @@ -3030,16 +3031,17 @@ class Eynollah: height3 =672#448 width3 = 448#224 - _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) - - img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') - - for j in range(len(cy_main)): - img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - - co_text_all = contours_only_text_parent + contours_only_text_parent_h + if contours_only_text_parent_h: + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + + for j in range(len(cy_main)): + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 + + co_text_all = contours_only_text_parent + contours_only_text_parent_h + else: + co_text_all = contours_only_text_parent labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') @@ -3118,7 +3120,7 @@ class Eynollah: tot_counter = tot_counter+1 - starting_list_of_regions, index_update = self.update_list_and_return_first_biger_than_one_length(index_update, i, pr_list, post_list,starting_list_of_regions) + starting_list_of_regions, index_update = self.update_list_and_return_first_with_length_bigger_than_one(index_update, i, pr_list, post_list,starting_list_of_regions) index_sort = [i[0] for i in starting_list_of_regions ] @@ -3138,7 +3140,7 @@ class Eynollah: """ self.logger.debug("enter run") - self.reading_order_machine_based = True#True + self.reading_order_machine_based = True#False#True#True t0_tot = time.time() @@ -3359,32 +3361,32 @@ class Eynollah: all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) pixel_lines = 6 + if not self.reading_order_machine_based: + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + if not self.reading_order_machine_based: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) #print(boxes_d,'boxes_d') #img_once = np.zeros((textline_mask_tot_d.shape[0],textline_mask_tot_d.shape[1])) From f2811ee46990e95de7f0534411546954744416db Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:32:06 +0200 Subject: [PATCH 152/412] add supported OS to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 47d81bc..b095edb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface ## Installation -Python versions `3.8-3.11` with Tensorflow versions >=`2.12` are currently supported. +Python versions `3.8-3.11` with Tensorflow versions >=`2.12` on Linux are currently supported. Unfortunately we can not currently support Windows or MacOS. +Windows users may be able to successfully run the tool through [WSL](https://learn.microsoft.com/en-us/windows/wsl/). For (limited) GPU support the CUDA toolkit needs to be installed. From 6018b354aa9ca5ac97d522f33afe1bff94b76ea5 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 27 Nov 2023 17:23:34 +0100 Subject: [PATCH 153/412] comment unnecessary print commands --- qurator/eynollah/eynollah.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 4b1b5e9..49422fa 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2071,7 +2071,6 @@ class Eynollah: arg_text_con = [] for ii in range(len(cx_text_only)): for jj in range(len(boxes)): - print(cx_text_only[ii],cy_text_only[ii],'markaz') if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) break @@ -2107,8 +2106,6 @@ class Eynollah: ref_point += len(id_of_texts) order_of_texts_tot = [] - print(len(contours_only_text_parent),'contours_only_text_parent') - print(len(order_by_con_main),'order_by_con_main') for tj1 in range(len(contours_only_text_parent)): order_of_texts_tot.append(int(order_by_con_main[tj1])) From e7d12d3549caaae7024e23fae0aa1cfaac8221ae Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 27 Nov 2023 20:18:24 +0100 Subject: [PATCH 154/412] first update for only images extraction --- qurator/eynollah/eynollah.py | 633 +++++++++++++++++++++-------------- 1 file changed, 374 insertions(+), 259 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 49422fa..2375ad3 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -195,6 +195,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.light_version = light_version + self.extract_only_images = True self.ignore_page_extraction = ignore_page_extraction self.pcgts = pcgts if not dir_in: @@ -225,6 +226,7 @@ class Eynollah: self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" + self.model_region_dir_p_ens_light_only_images_extraction = dir_models + "/eynollah-main-regions_20231127_672_org_ens_11_13_16_17_18" if self.textline_light: self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: @@ -249,7 +251,23 @@ class Eynollah: self.ls_imgs = os.listdir(self.dir_in) - if dir_in and not light_version: + if dir_in and self.extract_only_images: + config = tf.compat.v1.ConfigProto() + config.gpu_options.allow_growth = True + session = tf.compat.v1.Session(config=config) + set_session(session) + + self.model_page = self.our_load_model(self.model_page_dir) + self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) + #self.model_bin = self.our_load_model(self.model_dir_of_binarization) + #self.model_textline = self.our_load_model(self.model_textline_dir) + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) + #self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) + #self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + + self.ls_imgs = os.listdir(self.dir_in) + + if dir_in and not (light_version or self.extract_only_images): config = tf.compat.v1.ConfigProto() config.gpu_options.allow_growth = True session = tf.compat.v1.Session(config=config) @@ -267,6 +285,7 @@ class Eynollah: self.ls_imgs = os.listdir(self.dir_in) + def _cache_images(self, image_filename=None, image_pil=None): ret = {} @@ -462,6 +481,27 @@ class Eynollah: num_column_is_classified = True return img_new, num_column_is_classified + + def calculate_width_height_by_columns_extract_only_images(self, img, num_col, width_early, label_p_pred): + self.logger.debug("enter calculate_width_height_by_columns") + if num_col == 1: + img_w_new = 700 + elif num_col == 2: + img_w_new = 900 + elif num_col == 3: + img_w_new = 1500 + elif num_col == 4: + img_w_new = 1800 + elif num_col == 5: + img_w_new = 2200 + elif num_col == 6: + img_w_new = 2500 + img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) + + img_new = resize_image(img, img_h_new, img_w_new) + num_column_is_classified = True + + return img_new, num_column_is_classified def resize_image_with_column_classifier(self, is_image_enhanced, img_bin): self.logger.debug("enter resize_image_with_column_classifier") @@ -511,7 +551,7 @@ class Eynollah: is_image_enhanced = True return img, img_new, is_image_enhanced - + def resize_and_enhance_image_with_column_classifier(self,light_version): self.logger.debug("enter resize_and_enhance_image_with_column_classifier") dpi = self.dpi @@ -569,17 +609,22 @@ class Eynollah: num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) - - if dpi < DPI_THRESHOLD: - img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) - if light_version: - image_res = np.copy(img_new) + + if not self.extract_only_images: + if dpi < DPI_THRESHOLD: + img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) + if light_version: + image_res = np.copy(img_new) + else: + image_res = self.predict_enhancement(img_new) + is_image_enhanced = True else: - image_res = self.predict_enhancement(img_new) - is_image_enhanced = True + num_column_is_classified = True + image_res = np.copy(img) + is_image_enhanced = False else: - num_column_is_classified = True - image_res = np.copy(img) + img_new, num_column_is_classified = self.calculate_width_height_by_columns_extract_only_images(img, num_col, width_early, label_p_pred) + image_res = np.copy(img_new) is_image_enhanced = False self.logger.debug("exit resize_and_enhance_image_with_column_classifier") @@ -867,11 +912,13 @@ class Eynollah: seg_not_base = label_p_pred[0,:,:,4] ##seg2 = -label_p_pred[0,:,:,2] - - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 - - + if self.extract_only_images: + seg_not_base[seg_not_base>0.3] =1 + seg_not_base[seg_not_base<1] =0 + else: + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + seg_test = label_p_pred[0,:,:,1] ##seg2 = -label_p_pred[0,:,:,2] @@ -888,13 +935,10 @@ class Eynollah: seg_line[seg_line>0.1] =1 seg_line[seg_line<1] =0 - - seg_background = label_p_pred[0,:,:,0] - ##seg2 = -label_p_pred[0,:,:,2] - - - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 + if not self.extract_only_images: + seg_background = label_p_pred[0,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 ##seg = seg+seg2 #seg = label_p_pred[0,:,:,2] #seg[seg>0.4] =1 @@ -908,7 +952,8 @@ class Eynollah: #seg[seg==1]=0 #seg[seg_test==1]=1 seg[seg_not_base==1]=4 - seg[seg_background==1]=0 + if not self.extract_only_images: + seg[seg_background==1]=0 seg[(seg_line==1) & (seg==0)]=3 seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) @@ -1573,6 +1618,60 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) + + def get_regions_light_v_extract_only_images(self,img,is_image_enhanced, num_col_classifier): + self.logger.debug("enter get_regions_light_v") + erosion_hurts = False + img_org = np.copy(img) + img_height_h = img_org.shape[0] + img_width_h = img_org.shape[1] + + #model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + + + img_resized = np.copy(img) + + + + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light_only_images_extraction) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region) + else: + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region) + + #plt.imshow(prediction_regions_org[:,:,0]) + #plt.show() + + prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) + + + prediction_regions_org=prediction_regions_org[:,:,0] + + mask_lines_only = (prediction_regions_org[:,:] ==3)*1 + + mask_texts_only = (prediction_regions_org[:,:] ==1)*1 + + mask_images_only=(prediction_regions_org[:,:] ==2)*1 + + polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) + polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + + + polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) + + polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) + + text_regions_p_true = np.zeros(prediction_regions_org.shape) + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) + + text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) + + polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2) + + return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_light_v") erosion_hurts = False @@ -2824,6 +2923,8 @@ class Eynollah: Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") + + self.extract_only_images = True t0_tot = time.time() @@ -2836,272 +2937,286 @@ class Eynollah: if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) - t1 = time.time() - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + if self.extract_only_images: + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) + + text_regions_p_1 ,erosion_hurts, polygons_lines_xml,polygons_of_images = self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, img_res) + #plt.imshow(text_regions_p_1) + #plt.show() + else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) + t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) else: - return pcgts + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) + + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + t1 = time.time() + #plt.imshow(table_prediction) + #plt.show() - if self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - - - min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] - index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + if self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + + min_con_area = 0.000005 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] + index_con_parents = np.argsort(areas_cnt_text_parent) + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] - else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > min_con_area] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > min_con_area] - index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + index_con_parents = np.argsort(areas_cnt_text_parent) + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass - if self.light_version: - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) - else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - - if not self.curved_line: + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) + # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) + else: + pass if self.light_version: - if self.textline_light: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - else: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) else: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + if not self.curved_line: + if self.light_version: + if self.textline_light: + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + else: + + scale_param = 1 + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + + if self.plotter: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) + pixel_lines = 6 + + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + + + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + + #print(boxes_d,'boxes_d') + #img_once = np.zeros((textline_mask_tot_d.shape[0],textline_mask_tot_d.shape[1])) + #for box_i in boxes_d: + #img_once[int(box_i[2]):int(box_i[3]),int(box_i[0]):int(box_i[1]) ] =1 + #plt.imshow(img_once) + #plt.show() + #print(np.unique(img_once),'img_once') if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) - pixel_lines = 6 - - - if not self.headers_off: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + if self.full_layout: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + self.logger.info("Job done in %.1fs", time.time() - t0) + ##return pcgts + else: + contours_only_text_parent_h = None if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - - #print(boxes_d,'boxes_d') - #img_once = np.zeros((textline_mask_tot_d.shape[0],textline_mask_tot_d.shape[1])) - #for box_i in boxes_d: - #img_once[int(box_i[2]):int(box_i[3]),int(box_i[0]):int(box_i[1]) ] =1 - #plt.imshow(img_once) - #plt.show() - #print(np.unique(img_once),'img_once') - if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() - if self.full_layout: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) - self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts - else: - contours_only_text_parent_h = None - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) - self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts - self.writer.write_pagexml(pcgts) - #self.logger.info("Job done in %.1fs", time.time() - t0) + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) + self.logger.info("Job done in %.1fs", time.time() - t0) + ##return pcgts + self.writer.write_pagexml(pcgts) + #self.logger.info("Job done in %.1fs", time.time() - t0) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From 6aac0b8fafb74046a7c1f5d11419f16b3c2d15ff Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 27 Nov 2023 22:12:50 +0100 Subject: [PATCH 155/412] avoiding artifact images on the boundary of documents --- qurator/eynollah/eynollah.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2375ad3..0c11327 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1669,9 +1669,39 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2) + polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.0001) - return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images + image_boundary_of_doc = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) + + image_boundary_of_doc[:20, :] = 1 + image_boundary_of_doc[text_regions_p_true.shape[0]-20:text_regions_p_true.shape[0], :] = 1 + + image_boundary_of_doc[:, :20] = 1 + image_boundary_of_doc[:, text_regions_p_true.shape[1]-20:text_regions_p_true.shape[1]] = 1 + + #plt.imshow(image_boundary_of_doc) + #plt.show() + + polygons_of_images_fin = [] + for ploy_img_ind in polygons_of_images: + test_poly_image = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) + test_poly_image = cv2.fillPoly(test_poly_image, pts = [ploy_img_ind], color=(1,1,1)) + + test_poly_image = test_poly_image[:,:] + image_boundary_of_doc[:,:] + test_poly_image_intersected_area = ( test_poly_image[:,:]==2 )*1 + + test_poly_image_intersected_area = test_poly_image_intersected_area.sum() + + if test_poly_image_intersected_area==0: + polygons_of_images_fin.append(ploy_img_ind) + #plt.imshow(test_poly_image) + #plt.show() + + + + + + return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_light_v") erosion_hurts = False From 364ccacab2623b1b3c799aef2cfa8d23d408f33b Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 28 Nov 2023 00:50:45 +0100 Subject: [PATCH 156/412] adding extracting images only in cli --- qurator/eynollah/cli.py | 11 +++++++++++ qurator/eynollah/eynollah.py | 6 ++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a2a2ad0..a12a61d 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -67,6 +67,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="If set, will plot intermediary files and images", ) +@click.option( + "--extract_only_images/--disable-extracting_only_images", + "-eoi/-noeoi", + is_flag=True, + help="If a directory is given, only images in documents will be cropped and saved there and the other processing will not be done", +) @click.option( "--allow-enhancement/--no-allow-enhancement", "-ae/-noae", @@ -148,6 +154,7 @@ def main( save_layout, save_deskewed, save_all, + extract_only_images, save_page, enable_plotting, allow_enhancement, @@ -175,12 +182,16 @@ def main( if textline_light and not light_version: print('Error: You used -tll to enable light textline detection but -light is not enabled') sys.exit(1) + if extract_only_images and not ( save_images and enable_plotting): + print('Error: You used -eoi to enable extract images only mode but did not enable plotting with -ep and providing an output directory with -si') + sys.exit(1) eynollah = Eynollah( image_filename=image, dir_out=out, dir_in=dir_in, dir_models=model, dir_of_cropped_images=save_images, + extract_only_images=extract_only_images, dir_of_layout=save_layout, dir_of_deskewed=save_deskewed, dir_of_all=save_all, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 0c11327..deb178f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -148,6 +148,7 @@ class Eynollah: dir_out=None, dir_in=None, dir_of_cropped_images=None, + extract_only_images=False, dir_of_layout=None, dir_of_deskewed=None, dir_of_all=None, @@ -195,7 +196,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.light_version = light_version - self.extract_only_images = True + self.extract_only_images = extract_only_images self.ignore_page_extraction = ignore_page_extraction self.pcgts = pcgts if not dir_in: @@ -2953,9 +2954,6 @@ class Eynollah: Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - - self.extract_only_images = True - t0_tot = time.time() From aa41e4df2025fb3f81a79802573e8d84a3e253b1 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 28 Nov 2023 21:37:55 +0100 Subject: [PATCH 157/412] The contours of images can now be written in an XML file --- qurator/eynollah/cli.py | 4 +-- qurator/eynollah/eynollah.py | 48 +++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a12a61d..9aba31d 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -182,8 +182,8 @@ def main( if textline_light and not light_version: print('Error: You used -tll to enable light textline detection but -light is not enabled') sys.exit(1) - if extract_only_images and not ( save_images and enable_plotting): - print('Error: You used -eoi to enable extract images only mode but did not enable plotting with -ep and providing an output directory with -si') + if extract_only_images and (allow_enhancement or allow_scaling or light_version) : + print('Error: You used -eoi which can not be enabled alongside light_version -light or allow_scaling -as or allow_enhancement -ae') sys.exit(1) eynollah = Eynollah( image_filename=image, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index deb178f..5a8adeb 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -624,8 +624,11 @@ class Eynollah: image_res = np.copy(img) is_image_enhanced = False else: - img_new, num_column_is_classified = self.calculate_width_height_by_columns_extract_only_images(img, num_col, width_early, label_p_pred) - image_res = np.copy(img_new) + #img_new, num_column_is_classified = self.calculate_width_height_by_columns_extract_only_images(img, num_col, width_early, label_p_pred) + #image_res = np.copy(img_new) + #is_image_enhanced = True + num_column_is_classified = True + image_res = np.copy(img) is_image_enhanced = False self.logger.debug("exit resize_and_enhance_image_with_column_classifier") @@ -1621,16 +1624,27 @@ class Eynollah: box_sub.put(boxes_sub_new) def get_regions_light_v_extract_only_images(self,img,is_image_enhanced, num_col_classifier): - self.logger.debug("enter get_regions_light_v") + self.logger.debug("enter get_regions_extract_images_only") erosion_hurts = False img_org = np.copy(img) img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - #model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + if num_col_classifier == 1: + img_w_new = 700 + elif num_col_classifier == 2: + img_w_new = 900 + elif num_col_classifier == 3: + img_w_new = 1500 + elif num_col_classifier == 4: + img_w_new = 1800 + elif num_col_classifier == 5: + img_w_new = 2200 + elif num_col_classifier == 6: + img_w_new = 2500 + img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) - - img_resized = np.copy(img) + img_resized = resize_image(img,img_h_new, img_w_new ) @@ -1644,6 +1658,11 @@ class Eynollah: #plt.show() prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) + + image_page, page_coord, cont_page = self.extract_page() + + + prediction_regions_org = prediction_regions_org[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] prediction_regions_org=prediction_regions_org[:,:,0] @@ -1695,6 +1714,13 @@ class Eynollah: if test_poly_image_intersected_area==0: polygons_of_images_fin.append(ploy_img_ind) + + #x, y, w, h = cv2.boundingRect(ploy_img_ind) + #box = [x, y, w, h] + #_, page_coord = crop_image_inside_box(box, text_regions_p_true) + #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) + + #polygons_of_images_fin.append(np.array(cont_page)) #plt.imshow(test_poly_image) #plt.show() @@ -1702,7 +1728,7 @@ class Eynollah: - return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin + return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin, image_page, page_coord, cont_page def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_light_v") erosion_hurts = False @@ -2554,6 +2580,7 @@ class Eynollah: prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) return prediction_table_erode.astype(np.int16) + def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts): img_g = self.imread(grayscale=True, uint8=True) @@ -2970,13 +2997,16 @@ class Eynollah: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) - text_regions_p_1 ,erosion_hurts, polygons_lines_xml,polygons_of_images = self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + text_regions_p_1 ,erosion_hurts, polygons_lines_xml,polygons_of_images,image_page, page_coord, cont_page = self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) + + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, [], [], [], [], [], cont_page, [], []) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, img_res) #plt.imshow(text_regions_p_1) #plt.show() + + self.writer.write_pagexml(pcgts) else: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) From 7cbca79f1676da3bead00acaba44157dab5de05c Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 1 Dec 2023 23:40:47 +0100 Subject: [PATCH 158/412] replacing images cotour with bounding box --- qurator/eynollah/eynollah.py | 30 ++++++++++++++++-------------- qurator/eynollah/writer.py | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 5a8adeb..e3e3a20 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1693,17 +1693,18 @@ class Eynollah: image_boundary_of_doc = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) - image_boundary_of_doc[:20, :] = 1 - image_boundary_of_doc[text_regions_p_true.shape[0]-20:text_regions_p_true.shape[0], :] = 1 + ###image_boundary_of_doc[:6, :] = 1 + ###image_boundary_of_doc[text_regions_p_true.shape[0]-6:text_regions_p_true.shape[0], :] = 1 - image_boundary_of_doc[:, :20] = 1 - image_boundary_of_doc[:, text_regions_p_true.shape[1]-20:text_regions_p_true.shape[1]] = 1 + ###image_boundary_of_doc[:, :6] = 1 + ###image_boundary_of_doc[:, text_regions_p_true.shape[1]-6:text_regions_p_true.shape[1]] = 1 #plt.imshow(image_boundary_of_doc) #plt.show() polygons_of_images_fin = [] for ploy_img_ind in polygons_of_images: + """ test_poly_image = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) test_poly_image = cv2.fillPoly(test_poly_image, pts = [ploy_img_ind], color=(1,1,1)) @@ -1713,20 +1714,21 @@ class Eynollah: test_poly_image_intersected_area = test_poly_image_intersected_area.sum() if test_poly_image_intersected_area==0: - polygons_of_images_fin.append(ploy_img_ind) + ##polygons_of_images_fin.append(ploy_img_ind) - #x, y, w, h = cv2.boundingRect(ploy_img_ind) - #box = [x, y, w, h] - #_, page_coord = crop_image_inside_box(box, text_regions_p_true) + x, y, w, h = cv2.boundingRect(ploy_img_ind) + box = [x, y, w, h] + _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - #polygons_of_images_fin.append(np.array(cont_page)) - #plt.imshow(test_poly_image) - #plt.show() + polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) + """ + x, y, w, h = cv2.boundingRect(ploy_img_ind) + box = [x, y, w, h] + _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) + #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - - - + polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin, image_page, page_coord, cont_page def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index f537f65..4487af5 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -172,10 +172,18 @@ class EynollahXmlWriter(): page.add_ImageRegion(img_region) points_co = '' for lmm in range(len(found_polygons_text_region_img[mm])): - points_co += str(int((found_polygons_text_region_img[mm][lmm,0,0] + page_coord[2]) / self.scale_x)) - points_co += ',' - points_co += str(int((found_polygons_text_region_img[mm][lmm,0,1] + page_coord[0]) / self.scale_y)) - points_co += ' ' + try: + points_co += str(int((found_polygons_text_region_img[mm][lmm,0,0] + page_coord[2]) / self.scale_x)) + points_co += ',' + points_co += str(int((found_polygons_text_region_img[mm][lmm,0,1] + page_coord[0]) / self.scale_y)) + points_co += ' ' + except: + + points_co += str(int((found_polygons_text_region_img[mm][lmm][0] + page_coord[2])/ self.scale_x )) + points_co += ',' + points_co += str(int((found_polygons_text_region_img[mm][lmm][1] + page_coord[0])/ self.scale_y )) + points_co += ' ' + img_region.get_Coords().set_points(points_co[:-1]) for mm in range(len(polygons_lines_to_be_written_in_xml)): From eac18c553d6829cbb6c3c0d6ca1572977a2b3243 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 13 Dec 2023 01:44:51 +0100 Subject: [PATCH 159/412] machine based reading order as an argument --- qurator/eynollah/cli.py | 8 ++++++++ qurator/eynollah/eynollah.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a2a2ad0..a422df9 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -133,6 +133,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="if this parameter set to true, this tool would ignore page extraction", ) +@click.option( + "--reading_order_machine_based/--heuristic_reading_order", + "-romb/-hro", + is_flag=True, + help="if this parameter set to true, this tool would apply machine based reading order detection", +) @click.option( "--log-level", "-l", @@ -160,6 +166,7 @@ def main( allow_scaling, headers_off, light_version, + reading_order_machine_based, ignore_page_extraction, log_level ): @@ -197,6 +204,7 @@ def main( headers_off=headers_off, light_version=light_version, ignore_page_extraction=ignore_page_extraction, + reading_order_machine_based=reading_order_machine_based, ) eynollah.run() #pcgts = eynollah.run() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c008476..5e06734 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -165,6 +165,7 @@ class Eynollah: headers_off=False, light_version=False, ignore_page_extraction=False, + reading_order_machine_based=False, override_dpi=None, logger=None, pcgts=None, @@ -181,6 +182,7 @@ class Eynollah: self.dir_in = dir_in self.dir_of_all = dir_of_all self.dir_save_page = dir_save_page + self.reading_order_machine_based = reading_order_machine_based self.dir_of_deskewed = dir_of_deskewed self.dir_of_deskewed = dir_of_deskewed self.dir_of_cropped_images=dir_of_cropped_images @@ -226,7 +228,7 @@ class Eynollah: self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" - self.model_reading_order_machine_dir = dir_models + "/model_6_reading_order_machine_based" + self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" if self.textline_light: self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: @@ -3139,8 +3141,6 @@ class Eynollah: Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - - self.reading_order_machine_based = True#False#True#True t0_tot = time.time() From f09b7c1bef9e91f244232eb88fab48f59624f822 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:29:10 +0100 Subject: [PATCH 160/412] use tf1 compatibility for keras backend --- qurator/eynollah/eynollah.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 49422fa..c162af7 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -29,7 +29,8 @@ warnings.filterwarnings("ignore") from scipy.signal import find_peaks import matplotlib.pyplot as plt from scipy.ndimage import gaussian_filter1d -from tensorflow.python.keras.backend import set_session +# use tf1 compatibility for keras backend +from tensorflow.compat.v1.keras.backend import set_session from tensorflow.keras import layers from .utils.contour import ( From b3fa68439559479f2786c12482fd9270af9b4075 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:30:40 +0100 Subject: [PATCH 161/412] pin tf2 version to 2.12.1 until we fix keras compatibility --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 530dac2..f01d319 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 -tensorflow >=2.12.0 +tensorflow == 2.12.1 imutils >= 0.5.3 matplotlib setuptools >= 50 From 533736a3e355c37fbe7bea8c993c502992390f85 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:28:22 +0100 Subject: [PATCH 162/412] update supported Python+Tensorflow version combinations --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b095edb..2dc90ec 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface ## Installation -Python versions `3.8-3.11` with Tensorflow versions >=`2.12` on Linux are currently supported. Unfortunately we can not currently support Windows or MacOS. -Windows users may be able to successfully run the tool through [WSL](https://learn.microsoft.com/en-us/windows/wsl/). +Python versions `3.8-3.11` with Tensorflow versions `2.12-2.15` on Linux are currently supported. For (limited) GPU support the CUDA toolkit needs to be installed. From ba64282118cd4891067a69825d7e03614c4eada7 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 3 Apr 2024 19:58:24 +0200 Subject: [PATCH 163/412] Update README.md --- README.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2dc90ec..302880a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Eynollah -> Document Layout Analysis (segmentation) using pre-trained models and heuristics +> Document Layout Analysis with Deep Learning and Heuristics [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) [![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=shield)](https://circleci.com/gh/qurator-spk/eynollah) [![GH Actions Test](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml/badge.svg)](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml) [![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://opensource.org/license/apache-2-0/) +[![DOI](https://img.shields.io/badge/DOI-10.1145%2F3604951.3605513-red)](https://doi.org/10.1145/3604951.3605513) ![](https://user-images.githubusercontent.com/952378/102350683-8a74db80-3fa5-11eb-8c7e-f743f7d6eae2.jpg) @@ -14,16 +15,19 @@ * Support for various image optimization operations: * cropping (border detection), binarization, deskewing, dewarping, scaling, enhancing, resizing * Text line segmentation to bounding boxes or polygons (contours) including for curved lines and vertical text -* Detection of reading order +* Detection of reading order (left-to-right or right-to-left) * Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface +:warning: Eynollah development is currently focused on achieving high quality results for a wide variety of historical documents. +Processing can be very slow, with a lot of potential to improve. We aim to work on this too, but contributions are always welcome. + ## Installation -Python versions `3.8-3.11` with Tensorflow versions `2.12-2.15` on Linux are currently supported. +Python `3.8-3.11` with Tensorflow `2.12-2.15` on Linux are currently supported. For (limited) GPU support the CUDA toolkit needs to be installed. -You can either install via +You can either install from PyPI ``` pip install eynollah @@ -39,18 +43,21 @@ cd eynollah; pip install -e . Alternatively, you can run `make install` or `make install-dev` for editable installation. ## Models -Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/). +Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/) or [huggingface](https://huggingface.co/SBB). -In case you want to train your own model to use with Eynollah, have a look at [sbb_pixelwise_segmentation](https://github.com/qurator-spk/sbb_pixelwise_segmentation). +## Train +🚧 **Work in progress** + +In case you want to train your own model, have a look at [`sbb_pixelwise_segmentation`](https://github.com/qurator-spk/sbb_pixelwise_segmentation). ## Usage The command-line interface can be called like this: ```sh eynollah \ - -i \ + -i | -di \ -o \ - -m \ + -m \ [OPTIONS] ``` @@ -67,7 +74,6 @@ The following options can be used to further configure the processing: | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | | `-ho` | ignore headers for reading order dectection | -| `-di ` | process all images in a directory in batch mode | | `-si ` | save image regions detected to this directory | | `-sd ` | save deskewed image to this directory | | `-sl ` | save layout prediction as plot to this directory | @@ -78,6 +84,7 @@ If no option is set, the tool will perform layout detection of main regions (bac The tool produces better quality output when RGB images are used as input than greyscale or binarized images. #### Use as OCR-D processor +🚧 **Work in progress** Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. @@ -95,11 +102,14 @@ ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps +#### Additional documentation +Please check the [wiki](https://github.com/qurator-spk/eynollah/wiki). + ## How to cite If you find this tool useful in your work, please consider citing our paper: ```bibtex -@inproceedings{rezanezhad2023eynollah, +@inproceedings{hip23rezanezhad, title = {Document Layout Analysis with Deep Learning and Heuristics}, author = {Rezanezhad, Vahid and Baierer, Konstantin and Gerber, Mike and Labusch, Kai and Neudecker, Clemens}, booktitle = {Proceedings of the 7th International Workshop on Historical Document Imaging and Processing {HIP} 2023, From 899bb9f00c3b14306eb96c2a4955a0d599cc175a Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:27:29 +0200 Subject: [PATCH 164/412] update GitHub actions --- .github/workflows/test-eynollah.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 30c9729..5a1acf4 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -14,8 +14,8 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 id: model_cache with: path: models_eynollah @@ -24,7 +24,7 @@ jobs: if: steps.model_cache.outputs.cache-hit != 'true' run: make models - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From f88ee99f3c8aea2772abdfef6b8cbc919682a794 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Thu, 23 May 2024 21:17:38 +0200 Subject: [PATCH 165/412] non-legacy namespace package --- qurator/__init__.py | 1 - qurator/eynollah/__init__.py | 1 - setup.py | 1 - 3 files changed, 3 deletions(-) delete mode 100644 qurator/__init__.py diff --git a/qurator/__init__.py b/qurator/__init__.py deleted file mode 100644 index 5284146..0000000 --- a/qurator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__("pkg_resources").declare_namespace(__name__) diff --git a/qurator/eynollah/__init__.py b/qurator/eynollah/__init__.py index 8b13789..e69de29 100644 --- a/qurator/eynollah/__init__.py +++ b/qurator/eynollah/__init__.py @@ -1 +0,0 @@ - diff --git a/setup.py b/setup.py index 9abf158..c78ee3f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ setup( author='Vahid Rezanezhad', url='https://github.com/qurator-spk/eynollah', license='Apache License 2.0', - namespace_packages=['qurator'], packages=find_packages(exclude=['tests']), install_requires=install_requires, package_data={ From 45bd76f5e81c305446750360c7ac62e38f454bac Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 24 May 2024 14:27:56 +0000 Subject: [PATCH 166/412] fix namespace pkg setup --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c78ee3f..af8a321 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import setup, find_namespace_packages from json import load install_requires = open('requirements.txt').read().split('\n') @@ -13,7 +13,7 @@ setup( author='Vahid Rezanezhad', url='https://github.com/qurator-spk/eynollah', license='Apache License 2.0', - packages=find_packages(exclude=['tests']), + packages=find_namespace_packages(include=['qurator']), install_requires=install_requires, package_data={ '': ['*.json'] From 514466883415f86ea90b6ef48a4e15187407ec05 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 17 Jul 2024 10:01:37 +0200 Subject: [PATCH 167/412] ocr engine first integration --- qurator/eynollah/cli.py | 8 + qurator/eynollah/eynollah.py | 295 ++++++++++++++++++++++++++++++++++- qurator/eynollah/writer.py | 15 +- 3 files changed, 313 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a422df9..833e904 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -139,6 +139,12 @@ from qurator.eynollah.eynollah import Eynollah is_flag=True, help="if this parameter set to true, this tool would apply machine based reading order detection", ) +@click.option( + "--do_ocr", + "-ocr/-noocr", + is_flag=True, + help="if this parameter set to true, this tool will try to do ocr", +) @click.option( "--log-level", "-l", @@ -167,6 +173,7 @@ def main( headers_off, light_version, reading_order_machine_based, + do_ocr, ignore_page_extraction, log_level ): @@ -205,6 +212,7 @@ def main( light_version=light_version, ignore_page_extraction=ignore_page_extraction, reading_order_machine_based=reading_order_machine_based, + do_ocr=do_ocr, ) eynollah.run() #pcgts = eynollah.run() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 5e06734..a505b0e 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -17,6 +17,16 @@ import gc from ocrd_utils import getLogger import cv2 import numpy as np +from transformers import TrOCRProcessor +from PIL import Image +import torch +from difflib import SequenceMatcher as sq +from transformers import VisionEncoderDecoderModel +from numba import cuda +import copy +from scipy.signal import find_peaks +from scipy.ndimage import gaussian_filter1d + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" stderr = sys.stderr sys.stderr = open(os.devnull, "w") @@ -166,6 +176,7 @@ class Eynollah: light_version=False, ignore_page_extraction=False, reading_order_machine_based=False, + do_ocr=False, override_dpi=None, logger=None, pcgts=None, @@ -199,6 +210,7 @@ class Eynollah: self.headers_off = headers_off self.light_version = light_version self.ignore_page_extraction = ignore_page_extraction + self.ocr = do_ocr self.pcgts = pcgts if not dir_in: self.plotter = None if not enable_plotting else EynollahPlotter( @@ -233,6 +245,9 @@ class Eynollah: self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: self.model_textline_dir = dir_models + "/eynollah-textline_20210425" + if self.ocr: + self.model_ocr_dir = dir_models + "/checkpoint-166692_printed_trocr" + self.model_tables = dir_models + "/eynollah-tables_20210319" self.models = {} @@ -251,6 +266,10 @@ class Eynollah: self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) + if self.ocr: + self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten")#("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") self.ls_imgs = os.listdir(self.dir_in) @@ -3135,6 +3154,223 @@ class Eynollah: return order_of_texts, id_of_texts + def return_start_and_end_of_common_text_of_textline_ocr(self,textline_image, ind_tot): + width = np.shape(textline_image)[1] + height = np.shape(textline_image)[0] + common_window = int(0.2*width) + + width1 = int ( width/2. - common_window ) + width2 = int ( width/2. + common_window ) + + img_sum = np.sum(textline_image[:,:,0], axis=0) + sum_smoothed = gaussian_filter1d(img_sum, 3) + + peaks_real, _ = find_peaks(sum_smoothed, height=0) + + if len(peaks_real)>70: + print(len(peaks_real), 'len(peaks_real)') + + peaks_real = peaks_real[(peaks_realwidth1)] + + arg_sort = np.argsort(sum_smoothed[peaks_real]) + + arg_sort4 =arg_sort[::-1][:4] + + peaks_sort_4 = peaks_real[arg_sort][::-1][:4] + + argsort_sorted = np.argsort(peaks_sort_4) + + first_4_sorted = peaks_sort_4[argsort_sorted] + y_4_sorted = sum_smoothed[peaks_real][arg_sort4[argsort_sorted]] + #print(first_4_sorted,'first_4_sorted') + + arg_sortnew = np.argsort(y_4_sorted) + peaks_final =np.sort( first_4_sorted[arg_sortnew][2:] ) + + #plt.figure(ind_tot) + #plt.imshow(textline_image) + #plt.plot([peaks_final[0], peaks_final[0]], [0, height-1]) + #plt.plot([peaks_final[1], peaks_final[1]], [0, height-1]) + #plt.savefig('./'+str(ind_tot)+'.png') + + return peaks_final[0], peaks_final[1] + else: + pass + + + def return_start_and_end_of_common_text_of_textline_ocr_without_common_section(self,textline_image, ind_tot): + width = np.shape(textline_image)[1] + height = np.shape(textline_image)[0] + common_window = int(0.06*width) + + width1 = int ( width/2. - common_window ) + width2 = int ( width/2. + common_window ) + + img_sum = np.sum(textline_image[:,:,0], axis=0) + sum_smoothed = gaussian_filter1d(img_sum, 3) + + peaks_real, _ = find_peaks(sum_smoothed, height=0) + + if len(peaks_real)>70: + #print(len(peaks_real), 'len(peaks_real)') + + peaks_real = peaks_real[(peaks_realwidth1)] + + arg_max = np.argmax(sum_smoothed[peaks_real]) + + peaks_final = peaks_real[arg_max] + + #plt.figure(ind_tot) + #plt.imshow(textline_image) + #plt.plot([peaks_final, peaks_final], [0, height-1]) + ##plt.plot([peaks_final[1], peaks_final[1]], [0, height-1]) + #plt.savefig('./'+str(ind_tot)+'.png') + + return peaks_final + else: + return None + def return_start_and_end_of_common_text_of_textline_ocr_new_splitted(self,peaks_real, sum_smoothed, start_split, end_split): + peaks_real = peaks_real[(peaks_realstart_split)] + + arg_sort = np.argsort(sum_smoothed[peaks_real]) + + arg_sort4 =arg_sort[::-1][:4] + + peaks_sort_4 = peaks_real[arg_sort][::-1][:4] + + argsort_sorted = np.argsort(peaks_sort_4) + + first_4_sorted = peaks_sort_4[argsort_sorted] + y_4_sorted = sum_smoothed[peaks_real][arg_sort4[argsort_sorted]] + #print(first_4_sorted,'first_4_sorted') + + arg_sortnew = np.argsort(y_4_sorted) + peaks_final =np.sort( first_4_sorted[arg_sortnew][3:] ) + return peaks_final[0] + + def return_start_and_end_of_common_text_of_textline_ocr_new(self,textline_image, ind_tot): + width = np.shape(textline_image)[1] + height = np.shape(textline_image)[0] + common_window = int(0.15*width) + + width1 = int ( width/2. - common_window ) + width2 = int ( width/2. + common_window ) + mid = int(width/2.) + + img_sum = np.sum(textline_image[:,:,0], axis=0) + sum_smoothed = gaussian_filter1d(img_sum, 3) + + peaks_real, _ = find_peaks(sum_smoothed, height=0) + + if len(peaks_real)>70: + peak_start = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted(peaks_real, sum_smoothed, width1, mid+2) + + peak_end = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted(peaks_real, sum_smoothed, mid-2, width2) + + #plt.figure(ind_tot) + #plt.imshow(textline_image) + #plt.plot([peak_start, peak_start], [0, height-1]) + #plt.plot([peak_end, peak_end], [0, height-1]) + #plt.savefig('./'+str(ind_tot)+'.png') + + return peak_start, peak_end + else: + pass + + def return_ocr_of_textline_without_common_section(self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + if h2w_ratio > 0.05: + pixel_values = processor(textline_image, return_tensors="pt").pixel_values + generated_ids = model_ocr.generate(pixel_values.to(device)) + generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] + else: + + #width = np.shape(textline_image)[1] + #height = np.shape(textline_image)[0] + #common_window = int(0.3*width) + + #width1 = int ( width/2. - common_window ) + #width2 = int ( width/2. + common_window ) + + + split_point = self.return_start_and_end_of_common_text_of_textline_ocr_without_common_section(textline_image, ind_tot) + if split_point: + image1 = textline_image[:, :split_point,:]# image.crop((0, 0, width2, height)) + image2 = textline_image[:, split_point:,:]#image.crop((width1, 0, width, height)) + + #pixel_values1 = processor(image1, return_tensors="pt").pixel_values + #pixel_values2 = processor(image2, return_tensors="pt").pixel_values + + pixel_values_merged = processor([image1,image2], return_tensors="pt").pixel_values + generated_ids_merged = model_ocr.generate(pixel_values_merged.to(device)) + generated_text_merged = processor.batch_decode(generated_ids_merged, skip_special_tokens=True) + + #print(generated_text_merged,'generated_text_merged') + + #generated_ids1 = model_ocr.generate(pixel_values1.to(device)) + #generated_ids2 = model_ocr.generate(pixel_values2.to(device)) + + #generated_text1 = processor.batch_decode(generated_ids1, skip_special_tokens=True)[0] + #generated_text2 = processor.batch_decode(generated_ids2, skip_special_tokens=True)[0] + + #generated_text = generated_text1 + ' ' + generated_text2 + generated_text = generated_text_merged[0] + ' ' + generated_text_merged[1] + + #print(generated_text1,'generated_text1') + #print(generated_text2, 'generated_text2') + #print('########################################') + else: + pixel_values = processor(textline_image, return_tensors="pt").pixel_values + generated_ids = model_ocr.generate(pixel_values.to(device)) + generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] + + #print(generated_text,'generated_text') + #print('########################################') + return generated_text + def return_ocr_of_textline(self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + if h2w_ratio > 0.05: + pixel_values = processor(textline_image, return_tensors="pt").pixel_values + generated_ids = model_ocr.generate(pixel_values.to(device)) + generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] + else: + #width = np.shape(textline_image)[1] + #height = np.shape(textline_image)[0] + #common_window = int(0.3*width) + + #width1 = int ( width/2. - common_window ) + #width2 = int ( width/2. + common_window ) + + try: + width1, width2 = self.return_start_and_end_of_common_text_of_textline_ocr_new(textline_image, ind_tot) + + image1 = textline_image[:, :width2,:]# image.crop((0, 0, width2, height)) + image2 = textline_image[:, width1:,:]#image.crop((width1, 0, width, height)) + + pixel_values1 = processor(image1, return_tensors="pt").pixel_values + pixel_values2 = processor(image2, return_tensors="pt").pixel_values + + generated_ids1 = model_ocr.generate(pixel_values1.to(device)) + generated_ids2 = model_ocr.generate(pixel_values2.to(device)) + + generated_text1 = processor.batch_decode(generated_ids1, skip_special_tokens=True)[0] + generated_text2 = processor.batch_decode(generated_ids2, skip_special_tokens=True)[0] + #print(generated_text1,'generated_text1') + #print(generated_text2, 'generated_text2') + #print('########################################') + + match = sq(None, generated_text1, generated_text2).find_longest_match(0, len(generated_text1), 0, len(generated_text2)) + + generated_text = generated_text1 + generated_text2[match.b+match.size:] + except: + pixel_values = processor(textline_image, return_tensors="pt").pixel_values + generated_ids = model_ocr.generate(pixel_values.to(device)) + generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] + + return generated_text + + def return_textline_contour_with_added_box_coordinate(self, textline_contour, box_ind): + textline_contour[:,0] = textline_contour[:,0] + box_ind[2] + textline_contour[:,1] = textline_contour[:,1] + box_ind[0] + return textline_contour def run(self): """ @@ -3398,6 +3634,7 @@ class Eynollah: if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) t_order = time.time() + if self.full_layout: if self.reading_order_machine_based: @@ -3425,11 +3662,67 @@ class Eynollah: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + img_poly_on_img = np.copy(image_page) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + ##cv2.imwrite(str(ind_tot)+'.png', img_croped) + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) + if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index f537f65..c69be9b 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -2,7 +2,7 @@ # pylint: disable=import-error from pathlib import Path import os.path - +import xml.etree.ElementTree as ET from .utils.xml import create_page_xml, xml_reading_order from .utils.counter import EynollahIdCounter @@ -12,6 +12,7 @@ from ocrd_models.ocrd_page import ( CoordsType, PcGtsType, TextLineType, + TextEquivType, TextRegionType, ImageRegionType, TableRegionType, @@ -93,11 +94,13 @@ class EynollahXmlWriter(): points_co += ' ' coords.set_points(points_co[:-1]) - def serialize_lines_in_region(self, text_region, all_found_textline_polygons, region_idx, page_coord, all_box_coord, slopes, counter): + def serialize_lines_in_region(self, text_region, all_found_textline_polygons, region_idx, page_coord, all_box_coord, slopes, counter, ocr_all_textlines_textregion): self.logger.debug('enter serialize_lines_in_region') for j in range(len(all_found_textline_polygons[region_idx])): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) + if ocr_all_textlines_textregion: + textline.set_TextEquiv( [ TextEquivType(Unicode=ocr_all_textlines_textregion[j]) ] ) text_region.add_TextLine(textline) region_bboxes = all_box_coord[region_idx] points_co = '' @@ -140,7 +143,7 @@ class EynollahXmlWriter(): with open(out_fname, 'w') as f: f.write(to_xml(pcgts)) - def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables): + def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables, ocr_all_textlines): self.logger.debug('enter build_pagexml_no_full_layout') # create the file structure @@ -159,7 +162,11 @@ class EynollahXmlWriter(): Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord)), ) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter) + if ocr_all_textlines: + ocr_textlines = ocr_all_textlines[mm] + else: + ocr_textlines = None + self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter, ocr_textlines) for mm in range(len(found_polygons_marginals)): marginal = TextRegionType(id=counter.next_region_id, type_='marginalia', From ad133e34251b0164cca059542240690762dfb7db Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:49:43 +0200 Subject: [PATCH 168/412] Update model download url --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 525e6c3..439b534 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,13 @@ models: models_eynollah models_eynollah: models_eynollah.tar.gz # tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz - tar xf 2022-04-05.SavedModel.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' + tar xf models_eynollah_renamed_savedmodel.tar.gz --transform 's/models_eynollah_renamed_savedmodel/models_eynollah/' models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' # Install with pip install: From 3cfa447e84027867798a4c358244ed9ce0095ae9 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:01:36 +0200 Subject: [PATCH 169/412] remove CircleCI --- .circleci/config.yml | 51 -------------------------------------------- README.md | 1 - 2 files changed, 52 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index d2b7057..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: 2 - -jobs: - - build-python37: - machine: - - image: ubuntu-2004:2023.02.1 - - steps: - - checkout - - restore_cache: - keys: - - model-cache - - run: make models - - save_cache: - key: model-cache - paths: - models_eynollah.tar.gz - models_eynollah - - run: - name: "Set Python Version" - command: pyenv install -s 3.7.16 && pyenv global 3.7.16 - - run: make install - - run: make smoke-test - - build-python38: - machine: - - image: ubuntu-2004:2023.02.1 - steps: - - checkout - - restore_cache: - keys: - - model-cache - - run: make models - - save_cache: - key: model-cache - paths: - models_eynollah.tar.gz - models_eynollah - - run: - name: "Set Python Version" - command: pyenv install -s 3.8.16 && pyenv global 3.8.16 - - run: make install - - run: make smoke-test - -workflows: - version: 2 - build: - jobs: - # - build-python37 - - build-python38 diff --git a/README.md b/README.md index 302880a..3b4f784 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ > Document Layout Analysis with Deep Learning and Heuristics [![PyPI Version](https://img.shields.io/pypi/v/eynollah)](https://pypi.org/project/eynollah/) -[![CircleCI Build Status](https://circleci.com/gh/qurator-spk/eynollah.svg?style=shield)](https://circleci.com/gh/qurator-spk/eynollah) [![GH Actions Test](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml/badge.svg)](https://github.com/qurator-spk/eynollah/actions/workflows/test-eynollah.yml) [![License: ASL](https://img.shields.io/github/license/qurator-spk/eynollah)](https://opensource.org/license/apache-2-0/) [![DOI](https://img.shields.io/badge/DOI-10.1145%2F3604951.3605513-red)](https://doi.org/10.1145/3604951.3605513) From 40f5408b1e576eb83983f28d4fcd68c298d79899 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 31 Jul 2024 20:02:56 +0200 Subject: [PATCH 170/412] improve huggingface url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b4f784..f7a0a77 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ cd eynollah; pip install -e . Alternatively, you can run `make install` or `make install-dev` for editable installation. ## Models -Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/) or [huggingface](https://huggingface.co/SBB). +Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/) or [huggingface](https://huggingface.co/SBB?search_models=eynollah). ## Train 🚧 **Work in progress** From 38698c66097e7f3793eb4143a0519d4b36aa053f Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:16:02 +0200 Subject: [PATCH 171/412] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7a0a77..b47eae3 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ * Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface -:warning: Eynollah development is currently focused on achieving high quality results for a wide variety of historical documents. -Processing can be very slow, with a lot of potential to improve. We aim to work on this too, but contributions are always welcome. +:warning: Eynollah development is currently focused on achieving the best possible quality of results for a wide variety of historical documents and therefore processing can be very slow. We aim to improve this, but contributions are always welcome. ## Installation Python `3.8-3.11` with Tensorflow `2.12-2.15` on Linux are currently supported. @@ -79,8 +78,8 @@ The following options can be used to further configure the processing: | `-sp ` | save cropped page image to this directory | | `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | -If no option is set, the tool will perform layout detection of main regions (background, text, images, separators and marginals). -The tool produces better quality output when RGB images are used as input than greyscale or binarized images. +If no option is set, the tool performs layout detection of main regions (background, text, images, separators and marginals). +Best quality output is produced when RGB images are used as input rather than greyscale or binarized images. #### Use as OCR-D processor 🚧 **Work in progress** From 8862df9156b73eae0c1afb43dd7082f4115555dd Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 31 Jul 2024 22:53:36 +0200 Subject: [PATCH 172/412] format options table --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b47eae3..a92ad87 100644 --- a/README.md +++ b/README.md @@ -61,22 +61,22 @@ eynollah \ The following options can be used to further configure the processing: -| option | description | -|----------|:-------------| -| `-fl` | full layout analysis including all steps and segmentation classes | -| `-light` | lighter and faster but simpler method for main region detection and deskewing | -| `-tab` | apply table detection | -| `-ae` | apply enhancement (the resulting image is saved to the output directory) | -| `-as` | apply scaling | -| `-cl` | apply contour detection for curved text lines instead of bounding boxes | -| `-ib` | apply binarization (the resulting image is saved to the output directory) | -| `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | -| `-ho` | ignore headers for reading order dectection | -| `-si ` | save image regions detected to this directory | -| `-sd ` | save deskewed image to this directory | -| `-sl ` | save layout prediction as plot to this directory | -| `-sp ` | save cropped page image to this directory | -| `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | +| option | description | +|-------------------|:-------------------------------------------------------------------------------| +| `-fl` | full layout analysis including all steps and segmentation classes | +| `-light` | lighter and faster but simpler method for main region detection and deskewing | +| `-tab` | apply table detection | +| `-ae` | apply enhancement (the resulting image is saved to the output directory) | +| `-as` | apply scaling | +| `-cl` | apply contour detection for curved text lines instead of bounding boxes | +| `-ib` | apply binarization (the resulting image is saved to the output directory) | +| `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | +| `-ho` | ignore headers for reading order dectection | +| `-si ` | save image regions detected to this directory | +| `-sd ` | save deskewed image to this directory | +| `-sl ` | save layout prediction as plot to this directory | +| `-sp ` | save cropped page image to this directory | +| `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | If no option is set, the tool performs layout detection of main regions (background, text, images, separators and marginals). Best quality output is produced when RGB images are used as input rather than greyscale or binarized images. From c9f63826c05d5ddf975174a6ae28e7f7d9912fc0 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:13:42 +0200 Subject: [PATCH 173/412] create draft pyproject.toml --- pyproject.toml.txt | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pyproject.toml.txt diff --git a/pyproject.toml.txt b/pyproject.toml.txt new file mode 100644 index 0000000..43d7093 --- /dev/null +++ b/pyproject.toml.txt @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=61.0", "setuptools-ocrd"] + +[project] +name = "eynollah" +version = "0.3.0" +authors = [ + {name = "Vahid Rezanezhad"}, + {name = "Staatsbibliothek zu Berlin - Preußischer Kulturbesitz"}, +] +description = "Document Layout Analysis" +readme = "README.md" +license.file = "LICENSE" +requires-python = ">=3.8" +keywords = ["document layout analysis", "image segmentation"] + +dynamic = ["dependencies"] + +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Image Processing", +] + +[project.scripts] +eynollah = "eynollah.eynollah.cli:main" +ocrd-eynollah-segment = "eynollah.eynollah.ocrd_cli:main" + +[project.urls] +Homepage = "https://github.com/qurator-spk/eynollah" +Repository = "https://github.com/qurator-spk/eynollah.git" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} From 7ded54a8d21b14fff3c4d048a33710910476b834 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:25:31 +0200 Subject: [PATCH 174/412] rename GH action --- .github/workflows/test-eynollah.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 5a1acf4..98ddc06 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python package +name: Test on: [push] From f0e7f75499577bea004bff5b7a3e8b5a673688a1 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:30:25 +0200 Subject: [PATCH 175/412] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a92ad87..1720f7f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ * Output in [PAGE-XML](https://github.com/PRImA-Research-Lab/PAGE-XML) * [OCR-D](https://github.com/qurator-spk/eynollah#use-as-ocr-d-processor) interface -:warning: Eynollah development is currently focused on achieving the best possible quality of results for a wide variety of historical documents and therefore processing can be very slow. We aim to improve this, but contributions are always welcome. +:warning: Development is currently focused on achieving the best possible quality of results for a wide variety of historical documents and therefore processing can be very slow. We aim to improve this, but contributions are welcome. ## Installation Python `3.8-3.11` with Tensorflow `2.12-2.15` on Linux are currently supported. @@ -79,7 +79,7 @@ The following options can be used to further configure the processing: | `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | If no option is set, the tool performs layout detection of main regions (background, text, images, separators and marginals). -Best quality output is produced when RGB images are used as input rather than greyscale or binarized images. +The best output quality is produced when RGB images are used as input rather than greyscale or binarized images. #### Use as OCR-D processor 🚧 **Work in progress** From 9170a9f21c795430e55473df4090e08fa04922a7 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 6 Aug 2024 16:11:32 +0200 Subject: [PATCH 176/412] only images extraction - update inference parameters --- qurator/eynollah/eynollah.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index e3e3a20..a5d7b38 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -260,7 +260,7 @@ class Eynollah: self.model_page = self.our_load_model(self.model_page_dir) self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) - #self.model_bin = self.our_load_model(self.model_dir_of_binarization) + self.model_bin = self.our_load_model(self.model_dir_of_binarization) #self.model_textline = self.our_load_model(self.model_textline_dir) self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) #self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) @@ -917,7 +917,8 @@ class Eynollah: ##seg2 = -label_p_pred[0,:,:,2] if self.extract_only_images: - seg_not_base[seg_not_base>0.3] =1 + #seg_not_base[seg_not_base>0.3] =1 + seg_not_base[seg_not_base>0.5] =1 seg_not_base[seg_not_base<1] =0 else: seg_not_base[seg_not_base>0.03] =1 @@ -955,7 +956,7 @@ class Eynollah: ##plt.show() #seg[seg==1]=0 #seg[seg_test==1]=1 - seg[seg_not_base==1]=4 + ###seg[seg_not_base==1]=4 if not self.extract_only_images: seg[seg_background==1]=0 seg[(seg_line==1) & (seg==0)]=3 @@ -1689,7 +1690,13 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.0001) + + + text_regions_p_true[text_regions_p_true.shape[0]-15:text_regions_p_true.shape[0], :] = 0 + text_regions_p_true[:, text_regions_p_true.shape[1]-15:text_regions_p_true.shape[1]] = 0 + + ##polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.0001) + polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.001) image_boundary_of_doc = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) From a62ae370c3ff37495383f8415620dc2cf5d44eb1 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 7 Aug 2024 02:21:01 +0200 Subject: [PATCH 177/412] new full layout model and early layout for 1&2 column images are integrated - light version --- qurator/eynollah/eynollah.py | 118 ++++++++++++++++++++++++++++++----- qurator/eynollah/writer.py | 16 ++++- 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index a505b0e..8032f1e 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -241,6 +241,8 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" + self.model_region_dir_p_1_2_sp_np = dir_models + "/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" if self.textline_light: self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" else: @@ -263,6 +265,8 @@ class Eynollah: self.model_bin = self.our_load_model(self.model_dir_of_binarization) self.model_textline = self.our_load_model(self.model_textline_dir) self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) + self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) + self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) @@ -1069,6 +1073,66 @@ class Eynollah: croped_page, page_coord = crop_image_inside_box(box, img) return croped_page, page_coord + def extract_text_regions_new(self, img, patches, cols): + self.logger.debug("enter extract_text_regions") + img_height_h = img.shape[0] + img_width_h = img.shape[1] + if not self.dir_in: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully_new if patches else self.model_region_dir_fully_np) + else: + model_region = self.model_region_fl_new if patches else self.model_region_fl_np + + if not patches: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + prediction_regions2 = None + else: + if cols == 1: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + + img = resize_image(img, int(img_height_h * 1000 / float(img_width_h)), 1000) + img = img.astype(np.uint8) + + if cols == 2: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 1300 / float(img_width_h)), 1300) + img = img.astype(np.uint8) + + if cols == 3: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 1600 / float(img_width_h)), 1600) + img = img.astype(np.uint8) + + if cols == 4: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 1900 / float(img_width_h)), 1900) + img = img.astype(np.uint8) + + if cols == 5: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 2200 / float(img_width_h)), 2200) + img = img.astype(np.uint8) + + if cols >= 6: + img = otsu_copy_binary(img) + img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 2500 / float(img_width_h)), 2500) + img = img.astype(np.uint8) + + marginal_of_patch_percent = 0.1 + + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) + + prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) + self.logger.debug("exit extract_text_regions") + return prediction_regions, prediction_regions + + def extract_text_regions(self, img, patches, cols): self.logger.debug("enter extract_text_regions") img_height_h = img.shape[0] @@ -1652,10 +1716,17 @@ class Eynollah: textline_mask_tot_ea = self.run_textline(img_bin) if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + if num_col_classifier == 1 or num_col_classifier == 2: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) + else: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) else: - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) + if num_col_classifier == 1 or num_col_classifier == 2: + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) + else: + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() @@ -2828,24 +2899,32 @@ class Eynollah: text_regions_p[:, :][text_regions_p[:, :] == 4] = 8 image_page = image_page.astype(np.uint8) - - regions_fully, regions_fully_only_drop = self.extract_text_regions(image_page, True, cols=num_col_classifier) - text_regions_p[:,:][regions_fully[:,:,0]==6]=6 - regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) - regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 + + regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, True, cols=num_col_classifier) + + # 6 is the separators lable in old full layout model + # 4 is the drop capital class in old full layout model + # in the new full layout drop capital is 3 and separators are 5 + + text_regions_p[:,:][regions_fully[:,:,0]==5]=6 + regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 3] = 4 + + #text_regions_p[:,:][regions_fully[:,:,0]==6]=6 + #regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) + #regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully) - regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) - if num_col_classifier > 2: - regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 - else: - regions_fully_np = filter_small_drop_capitals_from_no_patch_layout(regions_fully_np, text_regions_p) + ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) + ##if num_col_classifier > 2: + ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 + ##else: + ##regions_fully_np = filter_small_drop_capitals_from_no_patch_layout(regions_fully_np, text_regions_p) - regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) + ###regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) # plt.imshow(regions_fully[:,:,0]) # plt.show() text_regions_p[:, :][regions_fully[:, :, 0] == 4] = 4 - text_regions_p[:, :][regions_fully_np[:, :, 0] == 4] = 4 + ####text_regions_p[:, :][regions_fully_np[:, :, 0] == 4] = 4 #plt.imshow(text_regions_p) #plt.show() ####if not self.tables: @@ -3645,8 +3724,13 @@ class Eynollah: else: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None + + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index c69be9b..29caddc 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -208,7 +208,7 @@ class EynollahXmlWriter(): return pcgts - def build_pagexml_full_layout(self, found_polygons_text_region, found_polygons_text_region_h, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, found_polygons_text_region_img, found_polygons_tables, found_polygons_drop_capitals, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml): + def build_pagexml_full_layout(self, found_polygons_text_region, found_polygons_text_region_h, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, found_polygons_text_region_img, found_polygons_tables, found_polygons_drop_capitals, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, ocr_all_textlines): self.logger.debug('enter build_pagexml_full_layout') # create the file structure @@ -225,14 +225,24 @@ class EynollahXmlWriter(): textregion = TextRegionType(id=counter.next_region_id, type_='paragraph', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region[mm], page_coord))) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter) + + if ocr_all_textlines: + ocr_textlines = ocr_all_textlines[mm] + else: + ocr_textlines = None + self.serialize_lines_in_region(textregion, all_found_textline_polygons, mm, page_coord, all_box_coord, slopes, counter, ocr_textlines) self.logger.debug('len(found_polygons_text_region_h) %s', len(found_polygons_text_region_h)) for mm in range(len(found_polygons_text_region_h)): textregion = TextRegionType(id=counter.next_region_id, type_='header', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region_h[mm], page_coord))) page.add_TextRegion(textregion) - self.serialize_lines_in_region(textregion, all_found_textline_polygons_h, mm, page_coord, all_box_coord_h, slopes_h, counter) + + if ocr_all_textlines: + ocr_textlines = ocr_all_textlines[mm] + else: + ocr_textlines = None + self.serialize_lines_in_region(textregion, all_found_textline_polygons_h, mm, page_coord, all_box_coord_h, slopes_h, counter, ocr_textlines) for mm in range(len(found_polygons_marginals)): marginal = TextRegionType(id=counter.next_region_id, type_='marginalia', From be144db9f83fbdd0bd345b89f5634b419e0fd919 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 7 Aug 2024 18:13:10 +0200 Subject: [PATCH 178/412] updating 1&2 columns images + full layout --- qurator/eynollah/eynollah.py | 143 +++++++++++++++++++++-------- qurator/eynollah/utils/__init__.py | 14 ++- 2 files changed, 115 insertions(+), 42 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8032f1e..54e6e3b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1083,43 +1083,64 @@ class Eynollah: model_region = self.model_region_fl_new if patches else self.model_region_fl_np if not patches: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) prediction_regions2 = None else: if cols == 1: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 1000 / float(img_width_h)), 1000) img = img.astype(np.uint8) if cols == 2: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 1300 / float(img_width_h)), 1300) img = img.astype(np.uint8) if cols == 3: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 1600 / float(img_width_h)), 1600) img = img.astype(np.uint8) if cols == 4: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 1900 / float(img_width_h)), 1900) img = img.astype(np.uint8) if cols == 5: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 2200 / float(img_width_h)), 2200) img = img.astype(np.uint8) if cols >= 6: - img = otsu_copy_binary(img) + if self.light_version: + pass + else: + img = otsu_copy_binary(img) img = img.astype(np.uint8) img = resize_image(img, int(img_height_h * 2500 / float(img_width_h)), 2500) img = img.astype(np.uint8) @@ -1611,6 +1632,7 @@ class Eynollah: img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) + #print(img.shape,'bin shape') if not self.dir_in: prediction_textline = self.do_prediction(patches, img, model_textline) else: @@ -1664,6 +1686,7 @@ class Eynollah: box_sub.put(boxes_sub_new) def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_light_v") + t_in = time.time() erosion_hurts = False img_org = np.copy(img) img_height_h = img_org.shape[0] @@ -1671,7 +1694,7 @@ class Eynollah: #model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) - + #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: img_w_new = 1000 @@ -1711,9 +1734,12 @@ class Eynollah: #img= np.copy(prediction_bin) img_bin = np.copy(prediction_bin) - + #print("inside 1 ", time.time()-t_in) textline_mask_tot_ea = self.run_textline(img_bin) + + + #print("inside 2 ", time.time()-t_in) if not self.dir_in: if num_col_classifier == 1 or num_col_classifier == 2: @@ -1727,12 +1753,14 @@ class Eynollah: prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) else: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) - + + #print("inside 3 ", time.time()-t_in) #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) + img_bin = resize_image(img_bin,img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] @@ -1787,8 +1815,8 @@ class Eynollah: text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - - return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea + #print("inside 4 ", time.time()-t_in) + return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_from_xy_2models") @@ -2553,7 +2581,11 @@ class Eynollah: prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) return prediction_table_erode.astype(np.int16) - def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts): + def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light): + + #print(text_regions_p_1.shape, 'text_regions_p_1 shape run graphics') + #print(erosion_hurts, 'erosion_hurts') + t_in_gr = time.time() img_g = self.imread(grayscale=True, uint8=True) img_g3 = np.zeros((img_g.shape[0], img_g.shape[1], 3)) @@ -2563,7 +2595,7 @@ class Eynollah: img_g3[:, :, 2] = img_g[:, :] image_page, page_coord, cont_page = self.extract_page() - + #print("inside graphics 1 ", time.time() - t_in_gr) if self.tables: table_prediction = self.get_tables_from_model(image_page, num_col_classifier) else: @@ -2574,6 +2606,9 @@ class Eynollah: text_regions_p_1 = text_regions_p_1[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] textline_mask_tot_ea = textline_mask_tot_ea[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + + img_bin_light = img_bin_light[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + mask_images = (text_regions_p_1[:, :] == 2) * 1 mask_images = mask_images.astype(np.uint8) mask_images = cv2.erode(mask_images[:, :], KERNEL, iterations=10) @@ -2582,7 +2617,7 @@ class Eynollah: img_only_regions_with_sep = ((text_regions_p_1[:, :] != 3) & (text_regions_p_1[:, :] != 0)) * 1 img_only_regions_with_sep = img_only_regions_with_sep.astype(np.uint8) - + #print("inside graphics 2 ", time.time() - t_in_gr) if erosion_hurts: img_only_regions = np.copy(img_only_regions_with_sep[:,:]) else: @@ -2600,8 +2635,10 @@ class Eynollah: except Exception as why: self.logger.error(why) num_col = None - return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea + #print("inside graphics 3 ", time.time() - t_in_gr) + return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): + t_in_gr = time.time() img_g = self.imread(grayscale=True, uint8=True) img_g3 = np.zeros((img_g.shape[0], img_g.shape[1], 3)) @@ -2629,13 +2666,11 @@ class Eynollah: img_only_regions_with_sep = ((text_regions_p_1[:, :] != 3) & (text_regions_p_1[:, :] != 0)) * 1 img_only_regions_with_sep = img_only_regions_with_sep.astype(np.uint8) - if erosion_hurts: img_only_regions = np.copy(img_only_regions_with_sep[:,:]) else: img_only_regions = cv2.erode(img_only_regions_with_sep[:,:], KERNEL, iterations=6) - try: num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) num_col = num_col + 1 @@ -2682,6 +2717,7 @@ class Eynollah: return textline_mask_tot_ea def run_deskew(self, textline_mask_tot_ea): + #print(textline_mask_tot_ea.shape, 'textline_mask_tot_ea deskew') sigma = 2 main_page_deskew = True slope_deskew = return_deskew_slop(cv2.erode(textline_mask_tot_ea, KERNEL, iterations=2), sigma, main_page_deskew, plotter=self.plotter) @@ -2805,7 +2841,7 @@ class Eynollah: self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables - def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts): + def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light): self.logger.debug('enter run_boxes_full_layout') if self.tables: @@ -2900,20 +2936,23 @@ class Eynollah: image_page = image_page.astype(np.uint8) - regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, True, cols=num_col_classifier) + if self.light_version: + regions_fully, regions_fully_only_drop = self.extract_text_regions_new(img_bin_light, True, cols=num_col_classifier) + else: + regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, True, cols=num_col_classifier) # 6 is the separators lable in old full layout model # 4 is the drop capital class in old full layout model # in the new full layout drop capital is 3 and separators are 5 text_regions_p[:,:][regions_fully[:,:,0]==5]=6 - regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 3] = 4 + ###regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 3] = 4 #text_regions_p[:,:][regions_fully[:,:,0]==6]=6 - #regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) - #regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 - - regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully) + ##regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) + ##regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 + drop_capital_label_in_full_layout_model = 3 + regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 @@ -2923,7 +2962,7 @@ class Eynollah: ###regions_fully = boosting_headers_by_longshot_region_segmentation(regions_fully, regions_fully_np, img_only_regions) # plt.imshow(regions_fully[:,:,0]) # plt.show() - text_regions_p[:, :][regions_fully[:, :, 0] == 4] = 4 + text_regions_p[:, :][regions_fully[:, :, 0] == drop_capital_label_in_full_layout_model] = 4 ####text_regions_p[:, :][regions_fully_np[:, :, 0] == 4] = 4 #plt.imshow(text_regions_p) #plt.show() @@ -3463,22 +3502,41 @@ class Eynollah: self.ls_imgs = [1] for img_name in self.ls_imgs: + print(img_name) t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) - + #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) + + if num_col_classifier == 1 or num_col_classifier ==2: + if num_col_classifier == 1: + img_w_new = 1000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 1300 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + else: + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) else: text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) self.logger.info("Textregion detection took %.1fs ", time.time() - t1) @@ -3498,7 +3556,7 @@ class Eynollah: continue else: return pcgts - + #print("text region early in %.1fs", time.time() - t0) t1 = time.time() if not self.light_version: textline_mask_tot_ea = self.run_textline(image_page) @@ -3513,17 +3571,20 @@ class Eynollah: textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) t1 = time.time() if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) if self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts) + if not self.light_version: + img_bin_light = None + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - + #print("text region early 2 in %.1fs", time.time() - t0) ###min_con_area = 0.000005 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text, hir_on_text = return_contours_of_image(text_only) @@ -3625,13 +3686,16 @@ class Eynollah: # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) else: pass + + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - + #print("text region early 5 in %.1fs", time.time() - t0) if not self.curved_line: if self.light_version: if self.textline_light: @@ -3651,7 +3715,7 @@ class Eynollah: all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - + #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) @@ -3778,7 +3842,10 @@ class Eynollah: #print(x, y, w, h, h/float(w),'ratio') h2w_ratio = h/float(w) mask_poly = np.zeros(image_page.shape) - img_poly_on_img = np.copy(image_page) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) @@ -3805,8 +3872,10 @@ class Eynollah: pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) ##return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs", time.time() - t0) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index d2b2488..929669f 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -775,9 +775,8 @@ def put_drop_out_from_only_drop_model(layout_no_patch, layout1): return layout_no_patch -def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch): - - drop_only = (layout_in_patch[:, :, 0] == 4) * 1 +def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop_capital_label): + drop_only = (layout_in_patch[:, :, 0] == drop_capital_label) * 1 contours_drop, hir_on_drop = return_contours_of_image(drop_only) contours_drop_parent = return_parent_contours(contours_drop, hir_on_drop) @@ -786,13 +785,18 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch): contours_drop_parent = [contours_drop_parent[jz] for jz in range(len(contours_drop_parent)) if areas_cnt_text[jz] > 0.00001] - areas_cnt_text = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > 0.001] + areas_cnt_text = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > 0.00001] contours_drop_parent_final = [] for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) - layout_in_patch[y : y + h, x : x + w, 0] = 4 + + if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.4: + + layout_in_patch[y : y + h, x : x + w, 0] = drop_capital_label + else: + layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = drop_capital_label return layout_in_patch From 00bf2b64d016df86810ec2eed5799799c7a13fbd Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 7 Aug 2024 19:07:54 +0200 Subject: [PATCH 179/412] 1&2 column images only printspace --- qurator/eynollah/eynollah.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 54e6e3b..3f078b0 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3549,7 +3549,8 @@ class Eynollah: if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], []) + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t1) if self.dir_in: self.writer.write_pagexml(pcgts) From 8e2cdad1be6c7ad6577f495eab22495671f4428c Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 7 Aug 2024 23:22:27 +0200 Subject: [PATCH 180/412] extracting images only - avoid artifacts with heuristics --- qurator/eynollah/eynollah.py | 15 +++++++----- run_image_extraction_over_ppn_lists.py | 33 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 run_image_extraction_over_ppn_lists.py diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index a5d7b38..6c3fa3e 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1731,11 +1731,14 @@ class Eynollah: polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) """ x, y, w, h = cv2.boundingRect(ploy_img_ind) - box = [x, y, w, h] - _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) - #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - - polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) + if h < 150 or w < 150: + pass + else: + box = [x, y, w, h] + _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) + #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) + + polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin, image_page, page_coord, cont_page def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): @@ -3011,7 +3014,7 @@ class Eynollah: pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, [], [], [], [], [], cont_page, [], []) if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, img_res) + self.plotter.write_images_into_directory(polygons_of_images, image_page) #plt.imshow(text_regions_p_1) #plt.show() diff --git a/run_image_extraction_over_ppn_lists.py b/run_image_extraction_over_ppn_lists.py new file mode 100644 index 0000000..a890022 --- /dev/null +++ b/run_image_extraction_over_ppn_lists.py @@ -0,0 +1,33 @@ +import os +import sys + +dir_ppn = '/home/vahid/Documents/eynollah/ppn_list.txt' + + +with open(dir_ppn) as f: + ppn_list = f.readlines() + + +ppn_list = [ind.split('\n')[0] for ind in ppn_list] + +url_main = 'https://content.staatsbibliothek-berlin.de/dc/download/zip?ppn=PPN' + +out_result = './new_results_ppns2' + + +for ppn_ind in ppn_list: + url = url_main + ppn_ind + #curl -o ./ppn.zip "https://content.staatsbibliothek-berlin.de/dc/download/zip?ppn=PPN1762638355" + os.system("curl -o "+"./PPN_"+ppn_ind+".zip"+" "+url) + os.system("unzip "+"PPN_"+ppn_ind+".zip"+ " -d "+"PPN_"+ppn_ind) + os.system("rm -rf "+"PPN_"+ppn_ind+"/*.txt") + + os.system("mkdir "+out_result+'/'+"PPN_"+ppn_ind+"_out") + os.system("mkdir "+out_result+'/'+"PPN_"+ppn_ind+"_out_images") + command_eynollah = "eynollah -m /home/vahid/Downloads/models_eynollah_renamed_savedmodel -di "+"PPN_"+ppn_ind+" "+"-o "+out_result+'/'+"PPN_"+ppn_ind+"_out "+"-eoi "+"-ep -si "+out_result+'/'+"PPN_"+ppn_ind+"_out_images" + os.system(command_eynollah) + + os.system("rm -rf "+"PPN_"+ppn_ind+".zip") + os.system("rm -rf "+"PPN_"+ppn_ind) + #sys.exit() + From e3edb0ec30826541817263c0a4a52419fe430ca9 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 9 Aug 2024 02:23:17 +0200 Subject: [PATCH 181/412] update --- qurator/eynollah/cli.py | 8 +++++--- qurator/eynollah/eynollah.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index a2a2ad0..822db18 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -198,9 +198,11 @@ def main( light_version=light_version, ignore_page_extraction=ignore_page_extraction, ) - eynollah.run() - #pcgts = eynollah.run() - ##eynollah.writer.write_pagexml(pcgts) + if dir_in: + eynollah.run() + else: + pcgts = eynollah.run() + eynollah.writer.write_pagexml(pcgts) if __name__ == "__main__": main() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c162af7..7f5561c 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3091,7 +3091,8 @@ class Eynollah: pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts + if not self.dir_in: + return pcgts else: contours_only_text_parent_h = None if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -3101,8 +3102,11 @@ class Eynollah: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts - self.writer.write_pagexml(pcgts) - #self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + + if self.dir_in: + self.writer.write_pagexml(pcgts) + #self.logger.info("Job done in %.1fs", time.time() - t0) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From 23ac58405c1642413aa34f493c43ed279bda4945 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:47:32 +0200 Subject: [PATCH 182/412] update pyproject.toml --- pyproject.toml.txt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml.txt b/pyproject.toml.txt index 43d7093..760c040 100644 --- a/pyproject.toml.txt +++ b/pyproject.toml.txt @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0", "setuptools-ocrd"] +requires = ["setuptools>=61.0", "wheel", "setuptools-ocrd"] [project] name = "eynollah" @@ -30,9 +30,20 @@ classifiers = [ eynollah = "eynollah.eynollah.cli:main" ocrd-eynollah-segment = "eynollah.eynollah.ocrd_cli:main" +[project.readme] +file = "README.md" +content-type = "text/markdown" + [project.urls] Homepage = "https://github.com/qurator-spk/eynollah" Repository = "https://github.com/qurator-spk/eynollah.git" [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} + +[tool.setuptools.packages.find] +where = ["src"] +namespaces = false + +[tool.setuptools.package-data] +"*" = ["*.json", '*.yml', '*.xml', '*.xsd'] From e97677879638816ee12d0e1840b41e3e021ea9b2 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 14 Aug 2024 14:33:01 +0200 Subject: [PATCH 183/412] testing pyproject.toml --- pyproject.toml | 30 ++++++++++++++++ qurator/eynollah/cli.py | 80 +++++++++++++++++++++++++---------------- requirements.txt | 8 ----- setup.py | 28 --------------- 4 files changed, 80 insertions(+), 66 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..102f443 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "eynollah" +version = "1.2.3" + + + + +dependencies = [ + "ocrd >= 2.23.3", + "tensorflow >= 2.12.0", + "scikit-learn >= 0.23.2", + "imutils >= 0.5.3", + "numpy < 1.24.0", + "matplotlib", + "torch == 2.0.1", + "transformers == 4.30.2", + "numba == 0.58.1", +] + +[project.scripts] +eynollah = "qurator.eynollah.cli:main" + + +[tool.setuptools.packages.find] +where = ["."] +include = ["qurator"] diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 833e904..6c6561f 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -3,14 +3,60 @@ import click from ocrd_utils import initLogging, setOverrideLogLevel from qurator.eynollah.eynollah import Eynollah +@click.group() +def main(): + pass -@click.command() +@main.command() +@click.option( + "--dir_xml", + "-dx", + help="directory of GT page-xml files", + type=click.Path(exists=True, file_okay=False), +) + +@click.option( + "--dir_out_modal_image", + "-domi", + help="directory where ground truth images would be written", + type=click.Path(exists=True, file_okay=False), +) + +@click.option( + "--dir_out_classes", + "-docl", + help="directory where ground truth classes would be written", + type=click.Path(exists=True, file_okay=False), +) + +@click.option( + "--input_height", + "-ih", + help="input height", +) +@click.option( + "--input_width", + "-iw", + help="input width", +) +@click.option( + "--min_area_size", + "-min", + help="min area size of regions considered for reading order training.", +) + +def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, input_height, input_width, min_area_size): + xml_files_ind = os.listdir(dir_xml) + + +@main.command() @click.option( "--image", "-i", help="image filename", type=click.Path(exists=True, dir_okay=False), ) + @click.option( "--out", "-o", @@ -146,37 +192,13 @@ from qurator.eynollah.eynollah import Eynollah help="if this parameter set to true, this tool will try to do ocr", ) @click.option( - "--log-level", + "--log_level", "-l", type=click.Choice(['OFF', 'DEBUG', 'INFO', 'WARN', 'ERROR']), help="Override log level globally to this", ) -def main( - image, - out, - dir_in, - model, - save_images, - save_layout, - save_deskewed, - save_all, - save_page, - enable_plotting, - allow_enhancement, - curved_line, - textline_light, - full_layout, - tables, - right2left, - input_binary, - allow_scaling, - headers_off, - light_version, - reading_order_machine_based, - do_ocr, - ignore_page_extraction, - log_level -): + +def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, ignore_page_extraction, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() @@ -215,8 +237,6 @@ def main( do_ocr=do_ocr, ) eynollah.run() - #pcgts = eynollah.run() - ##eynollah.writer.write_pagexml(pcgts) if __name__ == "__main__": main() diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 530dac2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# ocrd includes opencv, numpy, shapely, click -ocrd >= 2.23.3 -numpy <1.24.0 -scikit-learn >= 0.23.2 -tensorflow >=2.12.0 -imutils >= 0.5.3 -matplotlib -setuptools >= 50 diff --git a/setup.py b/setup.py deleted file mode 100644 index 9abf158..0000000 --- a/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -from setuptools import setup, find_packages -from json import load - -install_requires = open('requirements.txt').read().split('\n') -with open('ocrd-tool.json', 'r', encoding='utf-8') as f: - version = load(f)['version'] - -setup( - name='eynollah', - version=version, - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - author='Vahid Rezanezhad', - url='https://github.com/qurator-spk/eynollah', - license='Apache License 2.0', - namespace_packages=['qurator'], - packages=find_packages(exclude=['tests']), - install_requires=install_requires, - package_data={ - '': ['*.json'] - }, - entry_points={ - 'console_scripts': [ - 'eynollah=qurator.eynollah.cli:main', - 'ocrd-eynollah-segment=qurator.eynollah.ocrd_cli:main', - ] - }, -) From 53fd5fb2a5da9a4c42bd1964a3ed1d2427f8637e Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 14 Aug 2024 14:42:37 +0200 Subject: [PATCH 184/412] resolving #106 for pyproject.toml test --- qurator/eynollah/cli.py | 6 +++++- qurator/eynollah/eynollah.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 6c6561f..b0f55cd 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -236,7 +236,11 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s reading_order_machine_based=reading_order_machine_based, do_ocr=do_ocr, ) - eynollah.run() + if dir_in: + eynollah.run() + else: + pcgts = eynollah.run() + eynollah.writer.write_pagexml(pcgts) if __name__ == "__main__": main() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 3f078b0..b27d269 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3797,7 +3797,8 @@ class Eynollah: pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts + if not self.dir_in: + return pcgts else: @@ -3872,9 +3873,11 @@ class Eynollah: self.logger.info("detection of reading order took %.1fs", time.time() - t_order) pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) - ##return pcgts + if not self.dir_in: + return pcgts #print("text region early 7 in %.1fs", time.time() - t0) - self.writer.write_pagexml(pcgts) + if self.dir_in: + self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) #print("Job done in %.1fs", time.time() - t0) From 4c50479cb87cf6abf29f1ce8f907eb6814eedec0 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 14 Aug 2024 15:28:36 +0200 Subject: [PATCH 185/412] pyproject.toml may work for ocrd --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 102f443..c76f7e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "eynollah" -version = "1.2.3" +version = "0.1.0" @@ -23,8 +23,12 @@ dependencies = [ [project.scripts] eynollah = "qurator.eynollah.cli:main" +ocrd-eynollah-segment="qurator.eynollah.ocrd_cli:main" [tool.setuptools.packages.find] where = ["."] include = ["qurator"] + +[tool.setuptools.package-data] +"*" = ["*.json", '*.yml', '*.xml', '*.xsd'] From 28ee1e527ea96ce992ebc534401ba171179de9f9 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:50:57 +0200 Subject: [PATCH 186/412] update pyproject.toml for v0.3.1 --- pyproject.toml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8f83249 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel", "setuptools-ocrd"] + +[project] +name = "eynollah" +version = "0.3.0" +authors = [ + {name = "Vahid Rezanezhad"}, + {name = "Staatsbibliothek zu Berlin - Preußischer Kulturbesitz"}, +] +description = "Document Layout Analysis" +readme = "README.md" +license.file = "LICENSE" +requires-python = ">=3.8" +keywords = ["document layout analysis", "image segmentation"] + +dynamic = ["dependencies"] + +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Image Processing", +] + +[project.scripts] +eynollah = "qurator.eynollah.cli:main" +ocrd-eynollah-segment = "qurator.eynollah.ocrd_cli:main" + +[project.urls] +Homepage = "https://github.com/qurator-spk/eynollah" +Repository = "https://github.com/qurator-spk/eynollah.git" + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} + +[tool.setuptools.packages.find] +where = ["qurator"] + +[tool.setuptools.package-data] +"*" = ["*.json", '*.yml', '*.xml', '*.xsd'] From 8f769663946c0074557a039bc5c8059ec9d410fc Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:51:48 +0200 Subject: [PATCH 187/412] update pyproject.toml for v0.3.1 --- pyproject.toml.txt | 49 ---------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 pyproject.toml.txt diff --git a/pyproject.toml.txt b/pyproject.toml.txt deleted file mode 100644 index 760c040..0000000 --- a/pyproject.toml.txt +++ /dev/null @@ -1,49 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0", "wheel", "setuptools-ocrd"] - -[project] -name = "eynollah" -version = "0.3.0" -authors = [ - {name = "Vahid Rezanezhad"}, - {name = "Staatsbibliothek zu Berlin - Preußischer Kulturbesitz"}, -] -description = "Document Layout Analysis" -readme = "README.md" -license.file = "LICENSE" -requires-python = ">=3.8" -keywords = ["document layout analysis", "image segmentation"] - -dynamic = ["dependencies"] - -classifiers = [ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Scientific/Engineering :: Image Processing", -] - -[project.scripts] -eynollah = "eynollah.eynollah.cli:main" -ocrd-eynollah-segment = "eynollah.eynollah.ocrd_cli:main" - -[project.readme] -file = "README.md" -content-type = "text/markdown" - -[project.urls] -Homepage = "https://github.com/qurator-spk/eynollah" -Repository = "https://github.com/qurator-spk/eynollah.git" - -[tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} - -[tool.setuptools.packages.find] -where = ["src"] -namespaces = false - -[tool.setuptools.package-data] -"*" = ["*.json", '*.yml', '*.xml', '*.xsd'] From 74eac4daccd7e5bd9dc5644dc01ad54671671a10 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 15 Aug 2024 13:50:36 +0200 Subject: [PATCH 188/412] dtype = object in the case of length 1 arise error --- qurator/eynollah/eynollah.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index b27d269..b4e7276 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3599,7 +3599,10 @@ class Eynollah: contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + if len(contours_only_text_parent)>1: + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + else: + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -3614,7 +3617,10 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + if len(contours_only_text_parent_d)>1: + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + else: + contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) @@ -3677,7 +3683,10 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + if len(contours_only_text_parent)>1: + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + else: + contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) @@ -3719,7 +3728,10 @@ class Eynollah: #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if len(contours_only_text_parent_d_ordered)>1: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + else: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) if self.light_version: text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: @@ -3809,7 +3821,10 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if len(contours_only_text_parent_d_ordered)>1: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + else: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) From 6f4205ba49e66ad99b1c18a95533d71447625faf Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 15 Aug 2024 16:08:45 +0200 Subject: [PATCH 189/412] update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c76f7e7..67544bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ version = "0.1.0" dependencies = [ "ocrd >= 2.23.3", - "tensorflow >= 2.12.0", + "tensorflow == 2.12.1", "scikit-learn >= 0.23.2", "imutils >= 0.5.3", "numpy < 1.24.0", From 4f8210de71935f9980c121f5eaae4df2722903d7 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 15 Aug 2024 23:23:48 +0200 Subject: [PATCH 190/412] update Makefile model location --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 525e6c3..440b0bd 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,14 @@ models: models_eynollah models_eynollah: models_eynollah.tar.gz # tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz - tar xf 2022-04-05.SavedModel.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' + # tar xf 2022-04-05.SavedModel.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' + tar xf models_eynollah.tar.gz models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' + wget https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz # Install with pip install: From 7f99526b9dae4aff85fa01092aeb921f8c699cf5 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 15 Aug 2024 23:59:18 +0200 Subject: [PATCH 191/412] update Makefile model location --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 439b534..4b43564 100644 --- a/Makefile +++ b/Makefile @@ -24,13 +24,15 @@ models: models_eynollah models_eynollah: models_eynollah.tar.gz # tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' # tar xf models_eynollah_renamed.tar.gz - tar xf models_eynollah_renamed_savedmodel.tar.gz --transform 's/models_eynollah_renamed_savedmodel/models_eynollah/' + # tar xf models_eynollah_renamed_savedmodel.tar.gz --transform 's/models_eynollah_renamed_savedmodel/models_eynollah/' + tar xf models_eynollah.tar.gz models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' + # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' + wget https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz # Install with pip install: From c10a525675690076c1d029a483c0ff997c0c0e17 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 23 Aug 2024 02:18:16 +0200 Subject: [PATCH 192/412] inference with batch size bigger than 1 --- qurator/eynollah/eynollah.py | 172 ++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 72 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index b4e7276..2bf57a4 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -548,11 +548,11 @@ class Eynollah: if self.input_binary: img = self.imread() if self.dir_in: - prediction_bin = self.do_prediction(True, img, self.model_bin) + prediction_bin = self.do_prediction(True, img, self.model_bin, n_batch_inference=5) else: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img, model_bin) + prediction_bin = self.do_prediction(True, img, model_bin, n_batch_inference=5) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 @@ -703,7 +703,7 @@ class Eynollah: return model, None - def do_prediction(self, patches, img, model, marginal_of_patch_percent=0.1): + def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1): self.logger.debug("enter do_prediction") img_height_model = model.layers[len(model.layers) - 1].output_shape[1] @@ -745,7 +745,17 @@ class Eynollah: nyf = img_h / float(height_mid) nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) - + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) for i in range(nxf): for j in range(nyf): if i == 0: @@ -766,59 +776,77 @@ class Eynollah: if index_y_u > img_h: index_y_u = img_h index_y_d = img_h - img_height_model + + list_i_s.append(i) + list_j_s.append(j) + list_x_u.append(index_x_u) + list_x_d.append(index_x_d) + list_y_d.append(index_y_d) + list_y_u.append(index_y_u) + - img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), - verbose=0) - seg = np.argmax(label_p_pred, axis=3)[0] - seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) - - if i == 0 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - #seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] - #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i == 0 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - #seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] - #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] - #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i == 0 and j != 0 and j != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - #seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j != 0 and j != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] - #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i != 0 and i != nxf - 1 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] - #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color - elif i != 0 and i != nxf - 1 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] - #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] - #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color - + img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] + + batch_indexer = batch_indexer + 1 + + if batch_indexer == n_batch_inference: + + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) prediction_true = prediction_true.astype(np.uint8) #del model #gc.collect() @@ -835,7 +863,7 @@ class Eynollah: img = img / float(255.0) img = resize_image(img, img_height_model, img_width_model) - label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2])) + label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] @@ -1147,7 +1175,7 @@ class Eynollah: marginal_of_patch_percent = 0.1 - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") @@ -1173,7 +1201,7 @@ class Eynollah: img2 = img2.astype(np.uint8) img2 = resize_image(img2, int(img_height_h * 0.7), int(img_width_h * 0.7)) marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent) + prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) if cols == 2: @@ -1181,7 +1209,7 @@ class Eynollah: img2 = img2.astype(np.uint8) img2 = resize_image(img2, int(img_height_h * 0.4), int(img_width_h * 0.4)) marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent) + prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) elif cols > 2: @@ -1189,7 +1217,7 @@ class Eynollah: img2 = img2.astype(np.uint8) img2 = resize_image(img2, int(img_height_h * 0.3), int(img_width_h * 0.3)) marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent) + prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) if cols == 2: @@ -1245,7 +1273,7 @@ class Eynollah: img= resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)) marginal_of_patch_percent = 0.1 - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent) + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 @@ -1634,9 +1662,9 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) #print(img.shape,'bin shape') if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline) + prediction_textline = self.do_prediction(patches, img, model_textline, n_batch_inference=4) else: - prediction_textline = self.do_prediction(patches, img, self.model_textline) + prediction_textline = self.do_prediction(patches, img, self.model_textline, n_batch_inference=4) prediction_textline = resize_image(prediction_textline, img_h, img_w) if not self.dir_in: prediction_textline_longshot = self.do_prediction(False, img, model_textline) @@ -1721,9 +1749,9 @@ class Eynollah: if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin) + prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin) + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 @@ -1870,9 +1898,9 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1])) if self.dir_in: - prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, 0.2) + prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, marginal_of_patch_percent=0.2) else: - prediction_regions_org2 = self.do_prediction(True, img, model_region, 0.2) + prediction_regions_org2 = self.do_prediction(True, img, model_region, marginal_of_patch_percent=0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) @@ -1905,9 +1933,9 @@ class Eynollah: else: if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin) + prediction_bin = self.do_prediction(True, img_org, model_bin, n_batch_inference=5) else: - prediction_bin = self.do_prediction(True, img_org, self.model_bin) + prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] @@ -1958,9 +1986,9 @@ class Eynollah: if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin) + prediction_bin = self.do_prediction(True, img_org, model_bin, n_batch_inference=5) else: - prediction_bin = self.do_prediction(True, img_org, self.model_bin) + prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] From 84d05bd0ae93c2fa09c3e5fa40caa8660241fffa Mon Sep 17 00:00:00 2001 From: kba Date: Fri, 23 Aug 2024 14:01:20 +0200 Subject: [PATCH 193/412] s,url,local_filename, --- qurator/eynollah/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index ccec456..1bd190e 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -42,7 +42,7 @@ class EynollahProcessor(Processor): page = pcgts.get_Page() # XXX loses DPI information # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') - image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename + image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename eynollah_kwargs = { 'dir_models': self.resolve_resource(self.parameter['models']), 'allow_enhancement': False, From 0a3f525f0a2c8efbdfe55c5a27c3e8ac526662f9 Mon Sep 17 00:00:00 2001 From: kba Date: Fri, 23 Aug 2024 18:19:28 +0200 Subject: [PATCH 194/412] port processor to core v3 --- qurator/eynollah/processor.py | 89 +++++++++++------------------------ requirements.txt | 2 +- 2 files changed, 29 insertions(+), 62 deletions(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 1bd190e..c8748af 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -1,68 +1,35 @@ -from json import loads -from pkg_resources import resource_string -from tempfile import NamedTemporaryFile -from pathlib import Path -from os.path import join - -from PIL import Image - +from typing import Optional +from ocrd.processor.ocrd_page_result import OcrdPageResult +from ocrd_models import OcrdPage from ocrd import Processor -from ocrd_modelfactory import page_from_file, exif_from_filename -from ocrd_models import OcrdFile, OcrdExif -from ocrd_models.ocrd_page import to_xml -from ocrd_utils import ( - getLogger, - MIMETYPE_PAGE, - assert_file_grp_cardinality, - make_file_id -) from .eynollah import Eynollah -from .utils.pil_cv2 import pil2cv - -OCRD_TOOL = loads(resource_string(__name__, 'ocrd-tool.json').decode('utf8')) class EynollahProcessor(Processor): - def __init__(self, *args, **kwargs): - kwargs['ocrd_tool'] = OCRD_TOOL['tools']['ocrd-eynollah-segment'] - kwargs['version'] = OCRD_TOOL['version'] - super().__init__(*args, **kwargs) + @property + def metadata_location(self) -> str: + return 'eynollah/ocrd-tool.json' - def process(self): - LOG = getLogger('eynollah') - assert_file_grp_cardinality(self.input_file_grp, 1) - assert_file_grp_cardinality(self.output_file_grp, 1) - for n, input_file in enumerate(self.input_files): - page_id = input_file.pageId or input_file.ID - LOG.info("INPUT FILE %s (%d/%d) ", page_id, n + 1, len(self.input_files)) - pcgts = page_from_file(self.workspace.download_file(input_file)) - LOG.debug('width %s height %s', pcgts.get_Page().imageWidth, pcgts.get_Page().imageHeight) - self.add_metadata(pcgts) - page = pcgts.get_Page() - # XXX loses DPI information - # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') - image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename - eynollah_kwargs = { - 'dir_models': self.resolve_resource(self.parameter['models']), - 'allow_enhancement': False, - 'curved_line': self.parameter['curved_line'], - 'full_layout': self.parameter['full_layout'], - 'allow_scaling': self.parameter['allow_scaling'], - 'headers_off': self.parameter['headers_off'], - 'tables': self.parameter['tables'], - 'override_dpi': self.parameter['dpi'], - 'logger': LOG, - 'pcgts': pcgts, - 'image_filename': image_filename - } - Eynollah(**eynollah_kwargs).run() - file_id = make_file_id(input_file, self.output_file_grp) - pcgts.set_pcGtsId(file_id) - self.workspace.add_file( - ID=file_id, - file_grp=self.output_file_grp, - pageId=page_id, - mimetype=MIMETYPE_PAGE, - local_filename=join(self.output_file_grp, file_id) + '.xml', - content=to_xml(pcgts)) + def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: + assert input_pcgts + assert input_pcgts[0] + pcgts = input_pcgts[0] + page = pcgts.get_Page() + # XXX loses DPI information + # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') + image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename + Eynollah( + dir_models=self.resolve_resource(self.parameter['models']), + allow_enhancement=False, + curved_line=self.parameter['curved_line'], + full_layout=self.parameter['full_layout'], + allow_scaling=self.parameter['allow_scaling'], + headers_off=self.parameter['headers_off'], + tables=self.parameter['tables'], + override_dpi=self.parameter['dpi'], + logger=self.logger, + pcgts=pcgts, + image_filename=image_filename + ).run() + return OcrdPageResult(pcgts) diff --git a/requirements.txt b/requirements.txt index f01d319..feeea99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 2.23.3 +ocrd >= 3.0.0a2 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow == 2.12.1 From 4a13781ef49cd964accabb41b583cd4083ce0293 Mon Sep 17 00:00:00 2001 From: kba Date: Fri, 23 Aug 2024 18:32:29 +0200 Subject: [PATCH 195/412] class Eynollah: add typing, consistent interface in CLI and OCR-D CLI --- qurator/eynollah/cli.py | 5 ++-- qurator/eynollah/eynollah.py | 58 +++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 822db18..99bf5ac 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -1,6 +1,6 @@ import sys import click -from ocrd_utils import initLogging, setOverrideLogLevel +from ocrd_utils import getLogger, initLogging, setOverrideLogLevel from qurator.eynollah.eynollah import Eynollah @@ -176,10 +176,11 @@ def main( print('Error: You used -tll to enable light textline detection but -light is not enabled') sys.exit(1) eynollah = Eynollah( + model, + getLogger('Eynollah'), image_filename=image, dir_out=out, dir_in=dir_in, - dir_models=model, dir_of_cropped_images=save_images, dir_of_layout=save_layout, dir_of_deskewed=save_deskewed, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 7f5561c..f80798b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -6,14 +6,18 @@ document layout analysis (segmentation) with output in PAGE-XML """ +from logging import Logger import math import os import sys import time +from typing import Optional import warnings from pathlib import Path from multiprocessing import Process, Queue, cpu_count import gc +from PIL.Image import Image +from ocrd import OcrdPage from ocrd_utils import getLogger import cv2 import numpy as np @@ -142,32 +146,32 @@ class PatchEncoder(layers.Layer): class Eynollah: def __init__( self, - dir_models, - image_filename=None, - image_pil=None, - image_filename_stem=None, - dir_out=None, - dir_in=None, - dir_of_cropped_images=None, - dir_of_layout=None, - dir_of_deskewed=None, - dir_of_all=None, - dir_save_page=None, - enable_plotting=False, - allow_enhancement=False, - curved_line=False, - textline_light=False, - full_layout=False, - tables=False, - right2left=False, - input_binary=False, - allow_scaling=False, - headers_off=False, - light_version=False, - ignore_page_extraction=False, - override_dpi=None, - logger=None, - pcgts=None, + dir_models : str, + logger : Logger, + image_filename : Optional[str] = None, + image_pil : Optional[Image] = None, + image_filename_stem : Optional[str] = None, + dir_out : Optional[str] = None, + dir_in : Optional[str] = None, + dir_of_cropped_images : Optional[str] = None, + dir_of_layout : Optional[str] = None, + dir_of_deskewed : Optional[str] = None, + dir_of_all : Optional[str] = None, + dir_save_page : Optional[str] = None, + enable_plotting : bool = False, + allow_enhancement : bool = False, + curved_line : bool = False, + textline_light : bool = False, + full_layout : bool = False, + tables : bool = False, + right2left : bool = False, + input_binary : bool = False, + allow_scaling : bool = False, + headers_off : bool = False, + light_version : bool = False, + ignore_page_extraction : bool = False, + override_dpi : Optional[int] = None, + pcgts : Optional[OcrdPage] = None, ): if not dir_in: if image_pil: @@ -213,7 +217,7 @@ class Eynollah: curved_line=self.curved_line, textline_light = self.textline_light, pcgts=pcgts) - self.logger = logger if logger else getLogger('eynollah') + self.logger = logger self.dir_models = dir_models self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425" From 9ce02a569e49fe21ddff01dc14261b3f0583789f Mon Sep 17 00:00:00 2001 From: kba Date: Fri, 23 Aug 2024 18:32:59 +0200 Subject: [PATCH 196/412] ocrd-tool: add "allow_enhancement" parameter --- qurator/eynollah/ocrd-tool.json | 31 ++++++++++++++++++------------- qurator/eynollah/processor.py | 6 +++--- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 8a2cb95..311ac21 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -29,11 +29,11 @@ "default": true, "description": "Try to detect all element subtypes, including drop-caps and headings" }, - "tables": { - "type": "boolean", - "default": false, - "description": "Try to detect table regions" - }, + "tables": { + "type": "boolean", + "default": false, + "description": "Try to detect table regions" + }, "curved_line": { "type": "boolean", "default": false, @@ -44,6 +44,11 @@ "default": false, "description": "check the resolution against the number of detected columns and if needed, scale the image up or down during layout detection (heuristic to improve quality and performance)" }, + "allow_enhancement": { + "type": "boolean", + "default": false, + "description": "if this parameter set to true, this tool would check that input image need resizing and enhancement or not." + }, "headers_off": { "type": "boolean", "default": false, @@ -51,14 +56,14 @@ } }, "resources": [ - { - "description": "models for eynollah (TensorFlow format)", - "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz", - "name": "default", - "size": 1761991295, - "type": "archive", - "path_in_archive": "models_eynollah" - } + { + "description": "models for eynollah (TensorFlow format)", + "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz", + "name": "default", + "size": 1761991295, + "type": "archive", + "path_in_archive": "models_eynollah" + } ] } } diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index c8748af..304524a 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -20,15 +20,15 @@ class EynollahProcessor(Processor): # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename Eynollah( - dir_models=self.resolve_resource(self.parameter['models']), - allow_enhancement=False, + self.resolve_resource(self.parameter['models']), + self.logger, + allow_enhancement=self.parameter['allow_enhancement'], curved_line=self.parameter['curved_line'], full_layout=self.parameter['full_layout'], allow_scaling=self.parameter['allow_scaling'], headers_off=self.parameter['headers_off'], tables=self.parameter['tables'], override_dpi=self.parameter['dpi'], - logger=self.logger, pcgts=pcgts, image_filename=image_filename ).run() From 04e79002b3daa3f4e69921e6b94b3d0a6ee48639 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 24 Aug 2024 12:54:19 +0200 Subject: [PATCH 197/412] making light version faster for 1 and 2 columns images --- qurator/eynollah/eynollah.py | 88 ++++++++++++++++++------ qurator/eynollah/utils/separate_lines.py | 16 ++--- 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2bf57a4..640db16 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -28,6 +28,7 @@ from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" +#os.environ['CUDA_VISIBLE_DEVICES'] = '-1' stderr = sys.stderr sys.stderr = open(os.devnull, "w") import tensorflow as tf @@ -299,17 +300,25 @@ class Eynollah: def _cache_images(self, image_filename=None, image_pil=None): ret = {} + t_c0 = time.time() if image_filename: ret['img'] = cv2.imread(image_filename) - self.dpi = check_dpi(image_filename) + if self.light_version: + self.dpi = 100 + else: + self.dpi = check_dpi(image_filename) else: ret['img'] = pil2cv(image_pil) - self.dpi = check_dpi(image_pil) + if self.light_version: + self.dpi = 100 + else: + self.dpi = check_dpi(image_pil) ret['img_grayscale'] = cv2.cvtColor(ret['img'], cv2.COLOR_BGR2GRAY) for prefix in ('', '_grayscale'): ret[f'img{prefix}_uint8'] = ret[f'img{prefix}'].astype(np.uint8) return ret def reset_file_name_dir(self, image_filename): + t_c = time.time() self._imgs = self._cache_images(image_filename=image_filename) self.image_filename = image_filename @@ -491,6 +500,27 @@ class Eynollah: num_column_is_classified = True return img_new, num_column_is_classified + + def calculate_width_height_by_columns_1_2(self, img, num_col, width_early, label_p_pred): + self.logger.debug("enter calculate_width_height_by_columns") + if num_col == 1: + img_w_new = 1300 + img_h_new = int(img.shape[0] / float(img.shape[1]) * 1300) + else: + img_w_new = 1500 + img_h_new = int(img.shape[0] / float(img.shape[1]) * 1500) + + if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: + img_new = np.copy(img) + num_column_is_classified = False + elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + img_new = np.copy(img) + num_column_is_classified = False + else: + img_new = resize_image(img, img_h_new, img_w_new) + num_column_is_classified = True + + return img_new, num_column_is_classified def resize_image_with_column_classifier(self, is_image_enhanced, img_bin): self.logger.debug("enter resize_image_with_column_classifier") @@ -600,16 +630,24 @@ class Eynollah: self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) if dpi < DPI_THRESHOLD: - img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) + if light_version and num_col in (1,2): + img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2(img, num_col, width_early, label_p_pred) + else: + img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) if light_version: image_res = np.copy(img_new) else: image_res = self.predict_enhancement(img_new) is_image_enhanced = True else: - num_column_is_classified = True - image_res = np.copy(img) - is_image_enhanced = False + if light_version and num_col in (1,2): + img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2(img, num_col, width_early, label_p_pred) + image_res = np.copy(img_new) + is_image_enhanced = True + else: + num_column_is_classified = True + image_res = np.copy(img) + is_image_enhanced = False self.logger.debug("exit resize_and_enhance_image_with_column_classifier") return is_image_enhanced, img, image_res, num_col, num_column_is_classified, img_bin @@ -1175,7 +1213,7 @@ class Eynollah: marginal_of_patch_percent = 0.1 - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent) + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=4) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") @@ -1280,7 +1318,10 @@ class Eynollah: def get_slopes_and_deskew_new_light(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): self.logger.debug("enter get_slopes_and_deskew_new") - num_cores = cpu_count() + if len(contours)>15: + num_cores = cpu_count() + else: + num_cores = 1 queue_of_all_params = Queue() processes = [] @@ -1554,8 +1595,6 @@ class Eynollah: mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) - # plt.imshow(mask_only_con_region) - # plt.show() if self.textline_light: all_text_region_raw = np.copy(textline_mask_tot_ea) @@ -1660,11 +1699,11 @@ class Eynollah: img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - #print(img.shape,'bin shape') + #print(img.shape,'bin shape textline') if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, n_batch_inference=4) + prediction_textline = self.do_prediction(patches, img, model_textline, n_batch_inference=3) else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, n_batch_inference=4) + prediction_textline = self.do_prediction(patches, img, self.model_textline, n_batch_inference=3) prediction_textline = resize_image(prediction_textline, img_h, img_w) if not self.dir_in: prediction_textline_longshot = self.do_prediction(False, img, model_textline) @@ -1747,11 +1786,14 @@ class Eynollah: img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) img_resized = resize_image(img,img_h_new, img_w_new ) + t_bin = time.time() if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) + prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=10) else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=10) + + #print("inside bin ", time.time()-t_bin) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 @@ -2710,10 +2752,10 @@ class Eynollah: return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction def run_enhancement(self,light_version): + t_in = time.time() self.logger.info("Resizing and enhancing image...") is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier(light_version) self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') - scale = 1 if is_image_enhanced: if self.allow_enhancement: @@ -2731,6 +2773,7 @@ class Eynollah: if self.allow_scaling: img_org, img_res, is_image_enhanced = self.resize_image_with_column_classifier(is_image_enhanced, img_bin) self.get_image_and_scales_after_enhancing(img_org, img_res) + #print("enhancement in ", time.time()-t_in) return img_res, is_image_enhanced, num_col_classifier, num_column_is_classified def run_textline(self, image_page): @@ -2748,7 +2791,8 @@ class Eynollah: #print(textline_mask_tot_ea.shape, 'textline_mask_tot_ea deskew') sigma = 2 main_page_deskew = True - slope_deskew = return_deskew_slop(cv2.erode(textline_mask_tot_ea, KERNEL, iterations=2), sigma, main_page_deskew, plotter=self.plotter) + n_total_angles = 30 + slope_deskew = return_deskew_slop(cv2.erode(textline_mask_tot_ea, KERNEL, iterations=2), sigma, n_total_angles, main_page_deskew, plotter=self.plotter) slope_first = 0 if self.plotter: @@ -2871,7 +2915,7 @@ class Eynollah: def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light): self.logger.debug('enter run_boxes_full_layout') - + t_full0 = time.time() if self.tables: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) @@ -2963,12 +3007,12 @@ class Eynollah: text_regions_p[:, :][text_regions_p[:, :] == 4] = 8 image_page = image_page.astype(np.uint8) - + #print("full inside 1", time.time()- t_full0) if self.light_version: regions_fully, regions_fully_only_drop = self.extract_text_regions_new(img_bin_light, True, cols=num_col_classifier) else: regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, True, cols=num_col_classifier) - + #print("full inside 2", time.time()- t_full0) # 6 is the separators lable in old full layout model # 4 is the drop capital class in old full layout model # in the new full layout drop capital is 3 and separators are 5 @@ -3012,6 +3056,7 @@ class Eynollah: img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) self.logger.debug('exit run_boxes_full_layout') + #print("full inside 3", time.time()- t_full0) return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables def our_load_model(self, model_file): @@ -3534,6 +3579,7 @@ class Eynollah: t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) + #print("text region early -11 in %.1fs", time.time() - t0) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) @@ -3922,7 +3968,7 @@ class Eynollah: if self.dir_in: self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) - #print("Job done in %.1fs", time.time() - t0) + print("Job done in %.1fs", time.time() - t0) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/qurator/eynollah/utils/separate_lines.py b/qurator/eynollah/utils/separate_lines.py index acdc2e9..1004a92 100644 --- a/qurator/eynollah/utils/separate_lines.py +++ b/qurator/eynollah/utils/separate_lines.py @@ -1569,7 +1569,7 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): # plt.show() return img_patch_ineterst_revised -def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): +def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=False, plotter=None): if main_page and plotter: plotter.save_plot_of_textline_density(img_patch_org) @@ -1626,7 +1626,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): ang_int=0 - angels=np.linspace(ang_int-22.5,ang_int+22.5,100) + angels=np.linspace(ang_int-22.5,ang_int+22.5,n_tot_angles) var_res=[] for rot in angels: @@ -1649,7 +1649,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): #plt.imshow(img_resized) #plt.show() - angels=np.linspace(-12,12,100)#np.array([0 , 45 , 90 , -45]) + angels=np.linspace(-12,12,n_tot_angles)#np.array([0 , 45 , 90 , -45]) var_res=[] @@ -1680,7 +1680,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): early_slope_edge=11 if abs(ang_int)>early_slope_edge and ang_int<0: - angels=np.linspace(-90,-12,100) + angels=np.linspace(-90,-12,n_tot_angles) var_res=[] for rot in angels: img_rot=rotate_image(img_resized,rot) @@ -1700,7 +1700,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): elif abs(ang_int)>early_slope_edge and ang_int>0: - angels=np.linspace(90,12,100) + angels=np.linspace(90,12,n_tot_angles) var_res=[] for rot in angels: img_rot=rotate_image(img_resized,rot) @@ -1719,7 +1719,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): except: ang_int=0 else: - angels=np.linspace(-25,25,60) + angels=np.linspace(-25,25,int(n_tot_angles/2.)+10) var_res=[] indexer=0 for rot in angels: @@ -1749,7 +1749,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): early_slope_edge=22 if abs(ang_int)>early_slope_edge and ang_int<0: - angels=np.linspace(-90,-25,60) + angels=np.linspace(-90,-25,int(n_tot_angles/2.)+10) var_res=[] @@ -1772,7 +1772,7 @@ def return_deskew_slop(img_patch_org, sigma_des, main_page=False, plotter=None): elif abs(ang_int)>early_slope_edge and ang_int>0: - angels=np.linspace(90,25,60) + angels=np.linspace(90,25,int(n_tot_angles/2.)+10) var_res=[] From 0d83db7bc4b18b459b1ae58899bcb25d8d10ada0 Mon Sep 17 00:00:00 2001 From: kba Date: Sat, 24 Aug 2024 16:46:25 +0200 Subject: [PATCH 198/412] update processor to the latest change in bertsky/core#14 --- qurator/eynollah/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 304524a..83fed0e 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -8,7 +8,7 @@ from .eynollah import Eynollah class EynollahProcessor(Processor): @property - def metadata_location(self) -> str: + def metadata_filename(self) -> str: return 'eynollah/ocrd-tool.json' def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: From 87adc4b0c69233f15a637be5841477ad6f905ece Mon Sep 17 00:00:00 2001 From: kba Date: Sat, 24 Aug 2024 16:51:52 +0200 Subject: [PATCH 199/412] ocrd interface: add light_mode parameter --- qurator/eynollah/ocrd-tool.json | 5 +++++ qurator/eynollah/processor.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 311ac21..28dd772 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -49,6 +49,11 @@ "default": false, "description": "if this parameter set to true, this tool would check that input image need resizing and enhancement or not." }, + "light mode": { + "type": "boolean", + "default": false, + "description": "lighter and faster but simpler method for main region detection and deskewing" + }, "headers_off": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 83fed0e..65122dd 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -14,6 +14,7 @@ class EynollahProcessor(Processor): def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: assert input_pcgts assert input_pcgts[0] + assert self.parameter pcgts = input_pcgts[0] page = pcgts.get_Page() # XXX loses DPI information @@ -24,6 +25,7 @@ class EynollahProcessor(Processor): self.logger, allow_enhancement=self.parameter['allow_enhancement'], curved_line=self.parameter['curved_line'], + light_version=self.parameter['light_mode'], full_layout=self.parameter['full_layout'], allow_scaling=self.parameter['allow_scaling'], headers_off=self.parameter['headers_off'], From 39b16e59781d683e0d15ec750b7055d0d5969460 Mon Sep 17 00:00:00 2001 From: kba Date: Sat, 24 Aug 2024 18:00:45 +0200 Subject: [PATCH 200/412] ocrd interface: add textline_light --- qurator/eynollah/ocrd-tool.json | 7 ++++++- qurator/eynollah/processor.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 28dd772..ef6230c 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -49,11 +49,16 @@ "default": false, "description": "if this parameter set to true, this tool would check that input image need resizing and enhancement or not." }, - "light mode": { + "light_mode": { "type": "boolean", "default": false, "description": "lighter and faster but simpler method for main region detection and deskewing" }, + "textline_light": { + "type": "boolean", + "default": false, + "description": "if this parameter set to true, this tool will try to return contoure of textlines instead of rectangle bounding box of textline with a faster method." + }, "headers_off": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 65122dd..c4d3cb2 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -11,6 +11,10 @@ class EynollahProcessor(Processor): def metadata_filename(self) -> str: return 'eynollah/ocrd-tool.json' + def setup(self) -> None: + if self.parameter['textline_light'] and not self.parameter['light_mode']: + raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection but parameter 'light_mode' is not enabled") + def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: assert input_pcgts assert input_pcgts[0] @@ -26,6 +30,7 @@ class EynollahProcessor(Processor): allow_enhancement=self.parameter['allow_enhancement'], curved_line=self.parameter['curved_line'], light_version=self.parameter['light_mode'], + textline_light=self.parameter['textline_light'], full_layout=self.parameter['full_layout'], allow_scaling=self.parameter['allow_scaling'], headers_off=self.parameter['headers_off'], From ddcc0198bdf0e16f649cc671ef0d25f38614a784 Mon Sep 17 00:00:00 2001 From: kba Date: Sat, 24 Aug 2024 18:05:21 +0200 Subject: [PATCH 201/412] ocrd interface: add right_to_left --- qurator/eynollah/ocrd-tool.json | 5 +++++ qurator/eynollah/processor.py | 1 + 2 files changed, 6 insertions(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index ef6230c..02a2a23 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -59,6 +59,11 @@ "default": false, "description": "if this parameter set to true, this tool will try to return contoure of textlines instead of rectangle bounding box of textline with a faster method." }, + "right_to_left": { + "type": "boolean", + "default": false, + "description": "if this parameter set to true, this tool will extract right-to-left reading order." + }, "headers_off": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index c4d3cb2..d1bc44a 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -30,6 +30,7 @@ class EynollahProcessor(Processor): allow_enhancement=self.parameter['allow_enhancement'], curved_line=self.parameter['curved_line'], light_version=self.parameter['light_mode'], + right2left=self.parameter['right_to_left'], textline_light=self.parameter['textline_light'], full_layout=self.parameter['full_layout'], allow_scaling=self.parameter['allow_scaling'], From d7caeb2b05a65b9747343d31b42f723f8f11db6e Mon Sep 17 00:00:00 2001 From: kba Date: Sat, 24 Aug 2024 18:11:15 +0200 Subject: [PATCH 202/412] ocrd interface: add ignore_page_extraction --- qurator/eynollah/ocrd-tool.json | 5 +++++ qurator/eynollah/processor.py | 1 + 2 files changed, 6 insertions(+) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 02a2a23..127b95b 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -39,6 +39,11 @@ "default": false, "description": "try to return contour of textlines instead of just rectangle bounding box. Needs more processing time" }, + "ignore_page_extraction": { + "type": "boolean", + "default": false, + "description": "if this parameter set to true, this tool would ignore page extraction" + }, "allow_scaling": { "type": "boolean", "default": false, diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index d1bc44a..9fcf2d5 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -31,6 +31,7 @@ class EynollahProcessor(Processor): curved_line=self.parameter['curved_line'], light_version=self.parameter['light_mode'], right2left=self.parameter['right_to_left'], + ignore_page_extraction=self.parameter['ignore_page_extraction'], textline_light=self.parameter['textline_light'], full_layout=self.parameter['full_layout'], allow_scaling=self.parameter['allow_scaling'], From 8dfecb70d4cdd3364bd64e8048275f4840d935ae Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 19 Jan 2024 16:17:02 +0000 Subject: [PATCH 203/412] adapt to ocrd>=2.54 url vs local_filename # Conflicts: # qurator/eynollah/processor.py --- qurator/eynollah/processor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 9fcf2d5..488715d 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -23,7 +23,11 @@ class EynollahProcessor(Processor): page = pcgts.get_Page() # XXX loses DPI information # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') - image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename + if not('://' in page.imageFilename): + image_filename = next(self.workspace.mets.find_files(local_filename=page.imageFilename)).local_filename + else: + # could be a URL with file:// or truly remote + image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename Eynollah( self.resolve_resource(self.parameter['models']), self.logger, From 3381e5a01561d08ca10ad253fba27779453e0982 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:33:49 +0100 Subject: [PATCH 204/412] adapt to OcrdFile.local_filename now :Path # Conflicts: # qurator/eynollah/processor.py --- qurator/eynollah/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 488715d..92a91c2 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -43,6 +43,6 @@ class EynollahProcessor(Processor): tables=self.parameter['tables'], override_dpi=self.parameter['dpi'], pcgts=pcgts, - image_filename=image_filename + image_filename=str(image_filename) ).run() return OcrdPageResult(pcgts) From 49c1a8f38478715395fdaa10f953c3eaee41df5a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 24 May 2024 14:29:57 +0000 Subject: [PATCH 205/412] fix namespace pkg setup From c37d95dedfac320b4e6f40880f1dc04e9ee7e0df Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Thu, 23 May 2024 21:19:33 +0200 Subject: [PATCH 206/412] non-legacy namespace package # Conflicts: # setup.py From 61bcb435ae57c6194b64df3d9678bcce811712e6 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 11 Jun 2023 22:14:41 +0200 Subject: [PATCH 207/412] processor: reuse loaded models across pages, use derived images # Conflicts: # qurator/eynollah/processor.py --- qurator/eynollah/processor.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 92a91c2..ea144e4 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -12,6 +12,8 @@ class EynollahProcessor(Processor): return 'eynollah/ocrd-tool.json' def setup(self) -> None: + # for caching models + self.models = None if self.parameter['textline_light'] and not self.parameter['light_mode']: raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection but parameter 'light_mode' is not enabled") @@ -21,14 +23,19 @@ class EynollahProcessor(Processor): assert self.parameter pcgts = input_pcgts[0] page = pcgts.get_Page() + # if not('://' in page.imageFilename): + # image_filename = next(self.workspace.mets.find_files(local_filename=page.imageFilename)).local_filename + # else: + # # could be a URL with file:// or truly remote + # image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename # XXX loses DPI information - # page_image, _, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') - if not('://' in page.imageFilename): - image_filename = next(self.workspace.mets.find_files(local_filename=page.imageFilename)).local_filename - else: - # could be a URL with file:// or truly remote - image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename - Eynollah( + page_image, _, _ = self.workspace.image_from_page( + page, page_id, + # avoid any features that would change the coordinate system: cropped,deskewed + # (the PAGE builder merely adds regions, so afterwards we would not know which to transform) + # also avoid binarization as models usually fare better on grayscale/RGB + feature_filter='cropped,deskewed,binarized') + eynollah = Eynollah( self.resolve_resource(self.parameter['models']), self.logger, allow_enhancement=self.parameter['allow_enhancement'], @@ -43,6 +50,12 @@ class EynollahProcessor(Processor): tables=self.parameter['tables'], override_dpi=self.parameter['dpi'], pcgts=pcgts, - image_filename=str(image_filename) - ).run() + image_filename=page.imageFilename, + image_pil=page_image + ) + if self.models is not None: + # reuse loaded models from previous page + eynollah.models = self.models + eynollah.run() + self.models = eynollah.models return OcrdPageResult(pcgts) From d98fa2a85b7411338ee102039503e4dc142eb068 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 28 May 2024 14:07:45 +0200 Subject: [PATCH 208/412] check_dpi: fix Pillow type detection From ecd202ea4c57ed09c78f4880f4592b435d36ed3e Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Mon, 26 Aug 2024 10:39:22 +0200 Subject: [PATCH 209/412] processor.py: Simplify import Co-authored-by: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> --- qurator/eynollah/processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index ea144e4..e163ecd 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -1,5 +1,5 @@ from typing import Optional -from ocrd.processor.ocrd_page_result import OcrdPageResult +from ocrd import OcrdPageResult from ocrd_models import OcrdPage from ocrd import Processor From d26079db850cafe41225b449408b337a605e32ab Mon Sep 17 00:00:00 2001 From: kba Date: Mon, 26 Aug 2024 10:40:15 +0200 Subject: [PATCH 210/412] procesor.py: simplify imports further --- qurator/eynollah/processor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index e163ecd..2a383d8 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -1,7 +1,6 @@ from typing import Optional -from ocrd import OcrdPageResult from ocrd_models import OcrdPage -from ocrd import Processor +from ocrd import Processor, OcrdPageResult from .eynollah import Eynollah From 7b92620a104d5ff5f72c2a1755466eab5bc05843 Mon Sep 17 00:00:00 2001 From: Konstantin Baierer Date: Mon, 26 Aug 2024 10:45:53 +0200 Subject: [PATCH 211/412] processor: no more DPI info lost Co-authored-by: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> --- qurator/eynollah/processor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 2a383d8..01dd797 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -27,7 +27,6 @@ class EynollahProcessor(Processor): # else: # # could be a URL with file:// or truly remote # image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename - # XXX loses DPI information page_image, _, _ = self.workspace.image_from_page( page, page_id, # avoid any features that would change the coordinate system: cropped,deskewed From aef46a4669fa3c34b5df17ded284d072f32d5a46 Mon Sep 17 00:00:00 2001 From: kba Date: Mon, 26 Aug 2024 11:31:13 +0200 Subject: [PATCH 212/412] require ocrd >= 3.0.0b1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index feeea99..edfbe76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 3.0.0a2 +ocrd >= 3.0.0b1 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow == 2.12.1 From 7ae6a8776fb3cddc9279680f40fc23bc9b4df946 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 26 Aug 2024 16:02:10 +0200 Subject: [PATCH 213/412] ignoring dpi check by light version --- qurator/eynollah/eynollah.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 640db16..ff35d6f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -504,11 +504,11 @@ class Eynollah: def calculate_width_height_by_columns_1_2(self, img, num_col, width_early, label_p_pred): self.logger.debug("enter calculate_width_height_by_columns") if num_col == 1: + img_w_new = 1000 + img_h_new = int(img.shape[0] / float(img.shape[1]) * 1000) + else: img_w_new = 1300 img_h_new = int(img.shape[0] / float(img.shape[1]) * 1300) - else: - img_w_new = 1500 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 1500) if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) @@ -1213,7 +1213,7 @@ class Eynollah: marginal_of_patch_percent = 0.1 - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=4) + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=3) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") @@ -1810,7 +1810,8 @@ class Eynollah: #print("inside 2 ", time.time()-t_in) - + + #print(img_resized.shape, num_col_classifier, "num_col_classifier") if not self.dir_in: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) From 9ae05754364ed815dd73d74d79edc00a9f65fef4 Mon Sep 17 00:00:00 2001 From: kba Date: Tue, 27 Aug 2024 14:52:01 +0200 Subject: [PATCH 214/412] :memo: changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da2e1c0..0fd3938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +Fixed: + + * regression in OCR-D processor, #106 + * Expected Ptrcv::UMat for argument 'contour', #110 + * Memory usage explosion with very narrow images (e.g. book spine), #67 + ## [0.3.0] - 2023-05-13 Changed: From a5c7f223d1713ac2770bafd08dd3fc6d4b8e29a3 Mon Sep 17 00:00:00 2001 From: kba Date: Tue, 27 Aug 2024 14:54:59 +0200 Subject: [PATCH 215/412] :package: v0.3.1 --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- qurator/eynollah/ocrd-tool.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd3938..cf6263d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Versioned according to [Semantic Versioning](http://semver.org/). ## Unreleased +## [0.3.1] - 2024-08-27 + Fixed: * regression in OCR-D processor, #106 @@ -123,6 +125,8 @@ Fixed: Initial release +[0.3.1]: ../../compare/v0.3.1...v0.3.0 +[0.3.0]: ../../compare/v0.3.0...v0.2.0 [0.2.0]: ../../compare/v0.2.0...v0.1.0 [0.1.0]: ../../compare/v0.1.0...v0.0.11 [0.0.11]: ../../compare/v0.0.11...v0.0.10 diff --git a/pyproject.toml b/pyproject.toml index 8f83249..d6f16b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel", "setuptools-ocrd"] [project] name = "eynollah" -version = "0.3.0" +version = "0.3.1" authors = [ {name = "Vahid Rezanezhad"}, {name = "Staatsbibliothek zu Berlin - Preußischer Kulturbesitz"}, diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 8a2cb95..4551168 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -1,5 +1,5 @@ { - "version": "0.3.0", + "version": "0.3.1", "git_url": "https://github.com/qurator-spk/eynollah", "tools": { "ocrd-eynollah-segment": { From 62314c453ce7cbe0c66061b88a0367d4163124a2 Mon Sep 17 00:00:00 2001 From: kba Date: Tue, 27 Aug 2024 15:04:57 +0200 Subject: [PATCH 216/412] fully transition to pyproject --- pyproject.toml | 3 +-- setup.py | 28 ++-------------------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d6f16b3..8f9f175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ requires = ["setuptools>=61.0", "wheel", "setuptools-ocrd"] [project] name = "eynollah" -version = "0.3.1" authors = [ {name = "Vahid Rezanezhad"}, {name = "Staatsbibliothek zu Berlin - Preußischer Kulturbesitz"}, @@ -14,7 +13,7 @@ license.file = "LICENSE" requires-python = ">=3.8" keywords = ["document layout analysis", "image segmentation"] -dynamic = ["dependencies"] +dynamic = ["dependencies", "version"] classifiers = [ "Development Status :: 4 - Beta", diff --git a/setup.py b/setup.py index af8a321..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,3 @@ -from setuptools import setup, find_namespace_packages -from json import load +from setuptools import setup -install_requires = open('requirements.txt').read().split('\n') -with open('ocrd-tool.json', 'r', encoding='utf-8') as f: - version = load(f)['version'] - -setup( - name='eynollah', - version=version, - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - author='Vahid Rezanezhad', - url='https://github.com/qurator-spk/eynollah', - license='Apache License 2.0', - packages=find_namespace_packages(include=['qurator']), - install_requires=install_requires, - package_data={ - '': ['*.json'] - }, - entry_points={ - 'console_scripts': [ - 'eynollah=qurator.eynollah.cli:main', - 'ocrd-eynollah-segment=qurator.eynollah.ocrd_cli:main', - ] - }, -) +setup() From 93005959e54abf5f67def79868b8fd8d8831e287 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 27 Aug 2024 18:13:46 +0200 Subject: [PATCH 217/412] inference batch size debugged --- qurator/eynollah/eynollah.py | 73 +++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index ff35d6f..f183dee 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -89,7 +89,7 @@ from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter from .writer import EynollahXmlWriter -MIN_AREA_REGION = 0.0005 +MIN_AREA_REGION = 0.00001 SLOPE_THRESHOLD = 0.13 RATIO_OF_TWO_MODEL_THRESHOLD = 95.50 #98.45: DPI_THRESHOLD = 298 @@ -182,6 +182,7 @@ class Eynollah: logger=None, pcgts=None, ): + self.light_version = light_version if not dir_in: if image_pil: self._imgs = self._cache_images(image_pil=image_pil) @@ -209,7 +210,6 @@ class Eynollah: self.input_binary = input_binary self.allow_scaling = allow_scaling self.headers_off = headers_off - self.light_version = light_version self.ignore_page_extraction = ignore_page_extraction self.ocr = do_ocr self.pcgts = pcgts @@ -828,7 +828,6 @@ class Eynollah: batch_indexer = batch_indexer + 1 if batch_indexer == n_batch_inference: - label_p_pred = model.predict(img_patch,verbose=0) seg = np.argmax(label_p_pred, axis=3) @@ -885,6 +884,65 @@ class Eynollah: batch_indexer = 0 img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + elif i==(nxf-1) and j==(nyf-1): + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + prediction_true = prediction_true.astype(np.uint8) #del model #gc.collect() @@ -1789,9 +1847,9 @@ class Eynollah: t_bin = time.time() if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=10) + prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=10) + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) #print("inside bin ", time.time()-t_bin) prediction_bin=prediction_bin[:,:,0] @@ -1808,7 +1866,6 @@ class Eynollah: textline_mask_tot_ea = self.run_textline(img_bin) - #print("inside 2 ", time.time()-t_in) #print(img_resized.shape, num_col_classifier, "num_col_classifier") @@ -1839,6 +1896,10 @@ class Eynollah: mask_texts_only = (prediction_regions_org[:,:] ==1)*1 + mask_texts_only = mask_texts_only.astype('uint8') + + mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=3) + mask_images_only=(prediction_regions_org[:,:] ==2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) From 9367f86483329f7771d2d63cb063107d258f5412 Mon Sep 17 00:00:00 2001 From: kba Date: Thu, 29 Aug 2024 17:06:39 +0200 Subject: [PATCH 218/412] remove setup.py stub completely --- setup.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 6068493..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() From 84b844203d7a1cb27fccefd19dee2869b0abe3b2 Mon Sep 17 00:00:00 2001 From: kba Date: Thu, 29 Aug 2024 17:11:29 +0200 Subject: [PATCH 219/412] switch from qurator namespace to src-layout --- ocrd-tool.json | 2 +- pyproject.toml | 6 +++--- qurator/.gitkeep | 0 {qurator => src}/eynollah/__init__.py | 0 {qurator => src}/eynollah/cli.py | 2 +- {qurator => src}/eynollah/eynollah.py | 0 {qurator => src}/eynollah/ocrd-tool.json | 0 {qurator => src}/eynollah/ocrd_cli.py | 0 {qurator => src}/eynollah/plot.py | 0 {qurator => src}/eynollah/processor.py | 0 {qurator => src}/eynollah/utils/__init__.py | 0 {qurator => src}/eynollah/utils/contour.py | 0 {qurator => src}/eynollah/utils/counter.py | 0 {qurator => src}/eynollah/utils/drop_capitals.py | 0 {qurator => src}/eynollah/utils/is_nan.py | 0 {qurator => src}/eynollah/utils/marginals.py | 0 {qurator => src}/eynollah/utils/pil_cv2.py | 0 {qurator => src}/eynollah/utils/resize.py | 0 {qurator => src}/eynollah/utils/rotate.py | 0 {qurator => src}/eynollah/utils/separate_lines.py | 0 {qurator => src}/eynollah/utils/xml.py | 0 {qurator => src}/eynollah/writer.py | 0 tests/test_counter.py | 2 +- tests/test_dpi.py | 2 +- tests/test_run.py | 2 +- tests/test_smoke.py | 12 ++++++------ tests/test_xml.py | 2 +- 27 files changed, 15 insertions(+), 15 deletions(-) delete mode 100644 qurator/.gitkeep rename {qurator => src}/eynollah/__init__.py (100%) rename {qurator => src}/eynollah/cli.py (99%) rename {qurator => src}/eynollah/eynollah.py (100%) rename {qurator => src}/eynollah/ocrd-tool.json (100%) rename {qurator => src}/eynollah/ocrd_cli.py (100%) rename {qurator => src}/eynollah/plot.py (100%) rename {qurator => src}/eynollah/processor.py (100%) rename {qurator => src}/eynollah/utils/__init__.py (100%) rename {qurator => src}/eynollah/utils/contour.py (100%) rename {qurator => src}/eynollah/utils/counter.py (100%) rename {qurator => src}/eynollah/utils/drop_capitals.py (100%) rename {qurator => src}/eynollah/utils/is_nan.py (100%) rename {qurator => src}/eynollah/utils/marginals.py (100%) rename {qurator => src}/eynollah/utils/pil_cv2.py (100%) rename {qurator => src}/eynollah/utils/resize.py (100%) rename {qurator => src}/eynollah/utils/rotate.py (100%) rename {qurator => src}/eynollah/utils/separate_lines.py (100%) rename {qurator => src}/eynollah/utils/xml.py (100%) rename {qurator => src}/eynollah/writer.py (100%) diff --git a/ocrd-tool.json b/ocrd-tool.json index 5c48493..711a192 120000 --- a/ocrd-tool.json +++ b/ocrd-tool.json @@ -1 +1 @@ -qurator/eynollah/ocrd-tool.json \ No newline at end of file +src/eynollah/ocrd-tool.json \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8f9f175..67a420d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ ] [project.scripts] -eynollah = "qurator.eynollah.cli:main" -ocrd-eynollah-segment = "qurator.eynollah.ocrd_cli:main" +eynollah = "eynollah.cli:main" +ocrd-eynollah-segment = "eynollah.ocrd_cli:main" [project.urls] Homepage = "https://github.com/qurator-spk/eynollah" @@ -37,7 +37,7 @@ Repository = "https://github.com/qurator-spk/eynollah.git" dependencies = {file = ["requirements.txt"]} [tool.setuptools.packages.find] -where = ["qurator"] +where = ["src"] [tool.setuptools.package-data] "*" = ["*.json", '*.yml', '*.xml', '*.xsd'] diff --git a/qurator/.gitkeep b/qurator/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/qurator/eynollah/__init__.py b/src/eynollah/__init__.py similarity index 100% rename from qurator/eynollah/__init__.py rename to src/eynollah/__init__.py diff --git a/qurator/eynollah/cli.py b/src/eynollah/cli.py similarity index 99% rename from qurator/eynollah/cli.py rename to src/eynollah/cli.py index 822db18..d61928f 100644 --- a/qurator/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -1,7 +1,7 @@ import sys import click from ocrd_utils import initLogging, setOverrideLogLevel -from qurator.eynollah.eynollah import Eynollah +from eynollah.eynollah import Eynollah @click.command() diff --git a/qurator/eynollah/eynollah.py b/src/eynollah/eynollah.py similarity index 100% rename from qurator/eynollah/eynollah.py rename to src/eynollah/eynollah.py diff --git a/qurator/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json similarity index 100% rename from qurator/eynollah/ocrd-tool.json rename to src/eynollah/ocrd-tool.json diff --git a/qurator/eynollah/ocrd_cli.py b/src/eynollah/ocrd_cli.py similarity index 100% rename from qurator/eynollah/ocrd_cli.py rename to src/eynollah/ocrd_cli.py diff --git a/qurator/eynollah/plot.py b/src/eynollah/plot.py similarity index 100% rename from qurator/eynollah/plot.py rename to src/eynollah/plot.py diff --git a/qurator/eynollah/processor.py b/src/eynollah/processor.py similarity index 100% rename from qurator/eynollah/processor.py rename to src/eynollah/processor.py diff --git a/qurator/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py similarity index 100% rename from qurator/eynollah/utils/__init__.py rename to src/eynollah/utils/__init__.py diff --git a/qurator/eynollah/utils/contour.py b/src/eynollah/utils/contour.py similarity index 100% rename from qurator/eynollah/utils/contour.py rename to src/eynollah/utils/contour.py diff --git a/qurator/eynollah/utils/counter.py b/src/eynollah/utils/counter.py similarity index 100% rename from qurator/eynollah/utils/counter.py rename to src/eynollah/utils/counter.py diff --git a/qurator/eynollah/utils/drop_capitals.py b/src/eynollah/utils/drop_capitals.py similarity index 100% rename from qurator/eynollah/utils/drop_capitals.py rename to src/eynollah/utils/drop_capitals.py diff --git a/qurator/eynollah/utils/is_nan.py b/src/eynollah/utils/is_nan.py similarity index 100% rename from qurator/eynollah/utils/is_nan.py rename to src/eynollah/utils/is_nan.py diff --git a/qurator/eynollah/utils/marginals.py b/src/eynollah/utils/marginals.py similarity index 100% rename from qurator/eynollah/utils/marginals.py rename to src/eynollah/utils/marginals.py diff --git a/qurator/eynollah/utils/pil_cv2.py b/src/eynollah/utils/pil_cv2.py similarity index 100% rename from qurator/eynollah/utils/pil_cv2.py rename to src/eynollah/utils/pil_cv2.py diff --git a/qurator/eynollah/utils/resize.py b/src/eynollah/utils/resize.py similarity index 100% rename from qurator/eynollah/utils/resize.py rename to src/eynollah/utils/resize.py diff --git a/qurator/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py similarity index 100% rename from qurator/eynollah/utils/rotate.py rename to src/eynollah/utils/rotate.py diff --git a/qurator/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py similarity index 100% rename from qurator/eynollah/utils/separate_lines.py rename to src/eynollah/utils/separate_lines.py diff --git a/qurator/eynollah/utils/xml.py b/src/eynollah/utils/xml.py similarity index 100% rename from qurator/eynollah/utils/xml.py rename to src/eynollah/utils/xml.py diff --git a/qurator/eynollah/writer.py b/src/eynollah/writer.py similarity index 100% rename from qurator/eynollah/writer.py rename to src/eynollah/writer.py diff --git a/tests/test_counter.py b/tests/test_counter.py index 8ef0756..42bf074 100644 --- a/tests/test_counter.py +++ b/tests/test_counter.py @@ -1,5 +1,5 @@ from tests.base import main -from qurator.eynollah.utils.counter import EynollahIdCounter +from eynollah.utils.counter import EynollahIdCounter def test_counter_string(): c = EynollahIdCounter() diff --git a/tests/test_dpi.py b/tests/test_dpi.py index 510ffc5..3376bf4 100644 --- a/tests/test_dpi.py +++ b/tests/test_dpi.py @@ -1,6 +1,6 @@ import cv2 from pathlib import Path -from qurator.eynollah.utils.pil_cv2 import check_dpi +from eynollah.utils.pil_cv2 import check_dpi from tests.base import main def test_dpi(): diff --git a/tests/test_run.py b/tests/test_run.py index b1137e7..2596dad 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -2,7 +2,7 @@ from os import environ from pathlib import Path from ocrd_utils import pushd_popd from tests.base import CapturingTestCase as TestCase, main -from qurator.eynollah.cli import main as eynollah_cli +from eynollah.cli import main as eynollah_cli testdir = Path(__file__).parent.resolve() diff --git a/tests/test_smoke.py b/tests/test_smoke.py index d069479..252213f 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,7 +1,7 @@ def test_utils_import(): - import qurator.eynollah.utils - import qurator.eynollah.utils.contour - import qurator.eynollah.utils.drop_capitals - import qurator.eynollah.utils.drop_capitals - import qurator.eynollah.utils.is_nan - import qurator.eynollah.utils.rotate + import eynollah.utils + import eynollah.utils.contour + import eynollah.utils.drop_capitals + import eynollah.utils.drop_capitals + import eynollah.utils.is_nan + import eynollah.utils.rotate diff --git a/tests/test_xml.py b/tests/test_xml.py index 8422fd1..09a6ddf 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -1,5 +1,5 @@ from pytest import main -from qurator.eynollah.utils.xml import create_page_xml +from eynollah.utils.xml import create_page_xml from ocrd_models.ocrd_page import to_xml PAGE_2019 = 'http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15' From dfc4ac2538654ef446beb69652ea64543db2cc93 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Fri, 30 Aug 2024 22:46:51 +0200 Subject: [PATCH 220/412] setuptools: fix (packages.find.where prevented finding namespace qurator) --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f83249..9e610c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,5 @@ Repository = "https://github.com/qurator-spk/eynollah.git" [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} -[tool.setuptools.packages.find] -where = ["qurator"] - [tool.setuptools.package-data] "*" = ["*.json", '*.yml', '*.xml', '*.xsd'] From 1e902571ead1b8493376e2ca7d1dc401aefd929d Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 1 Sep 2024 10:15:11 +0200 Subject: [PATCH 221/412] undo customizing metadata_filename (not correct with namespace pkg support in core) --- qurator/eynollah/processor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qurator/eynollah/processor.py b/qurator/eynollah/processor.py index 01dd797..fd7dd2a 100644 --- a/qurator/eynollah/processor.py +++ b/qurator/eynollah/processor.py @@ -6,10 +6,6 @@ from .eynollah import Eynollah class EynollahProcessor(Processor): - @property - def metadata_filename(self) -> str: - return 'eynollah/ocrd-tool.json' - def setup(self) -> None: # for caching models self.models = None From 17eafc1ccb3980f2bedb5183d45942fc83a838ba Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 1 Sep 2024 10:15:31 +0200 Subject: [PATCH 222/412] adapt tool json to v3 --- qurator/eynollah/ocrd-tool.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/ocrd-tool.json b/qurator/eynollah/ocrd-tool.json index 127b95b..2da970d 100644 --- a/qurator/eynollah/ocrd-tool.json +++ b/qurator/eynollah/ocrd-tool.json @@ -6,8 +6,8 @@ "executable": "ocrd-eynollah-segment", "categories": ["Layout analysis"], "description": "Segment page into regions and lines and do reading order detection with eynollah", - "input_file_grp": ["OCR-D-IMG", "OCR-D-SEG-PAGE", "OCR-D-GT-SEG-PAGE"], - "output_file_grp": ["OCR-D-SEG-LINE"], + "input_file_grp_cardinality": 1, + "output_file_grp_cardinality": 1, "steps": ["layout/segmentation/region", "layout/segmentation/line"], "parameters": { "models": { From fdedae24066e5a99a3c4584d9c583b9318c0077b Mon Sep 17 00:00:00 2001 From: kba Date: Mon, 2 Sep 2024 11:47:57 +0200 Subject: [PATCH 223/412] require ocrd>=3.0.0b4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index edfbe76..30d4c51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 3.0.0b1 +ocrd >= 3.0.0b4 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow == 2.12.1 From 0f87974b0c7a7bdfddd31ffa99b89c58c952ddcf Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 2 Sep 2024 16:21:07 +0200 Subject: [PATCH 224/412] writing drop capitals in xml output + and may resolve issue #110 --- qurator/eynollah/eynollah.py | 23 ++++++++++++----------- qurator/eynollah/writer.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index f183dee..1bb0eff 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3735,9 +3735,9 @@ class Eynollah: contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - if len(contours_only_text_parent)>1: + try: contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - else: + except: contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) @@ -3753,10 +3753,11 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - if len(contours_only_text_parent_d)>1: + try: contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - else: + except: contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) @@ -3819,9 +3820,9 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - if len(contours_only_text_parent)>1: + try: contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - else: + except: contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) @@ -3864,10 +3865,10 @@ class Eynollah: #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - if len(contours_only_text_parent_d_ordered)>1: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - else: + try: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + except: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) if self.light_version: text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: @@ -3957,9 +3958,9 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - if len(contours_only_text_parent_d_ordered)>1: + try: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - else: + except: contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) diff --git a/qurator/eynollah/writer.py b/qurator/eynollah/writer.py index 29caddc..8eb1027 100644 --- a/qurator/eynollah/writer.py +++ b/qurator/eynollah/writer.py @@ -136,6 +136,29 @@ class EynollahXmlWriter(): points_co += str(int((contour_textline[0][1] + region_bboxes[0]+page_coord[0])/self.scale_y)) points_co += ' ' coords.set_points(points_co[:-1]) + + def serialize_lines_in_dropcapital(self, text_region, all_found_textline_polygons, region_idx, page_coord, all_box_coord, slopes, counter, ocr_all_textlines_textregion): + self.logger.debug('enter serialize_lines_in_region') + for j in range(1): + coords = CoordsType() + textline = TextLineType(id=counter.next_line_id, Coords=coords) + if ocr_all_textlines_textregion: + textline.set_TextEquiv( [ TextEquivType(Unicode=ocr_all_textlines_textregion[j]) ] ) + text_region.add_TextLine(textline) + #region_bboxes = all_box_coord[region_idx] + points_co = '' + for idx_contour_textline, contour_textline in enumerate(all_found_textline_polygons[j]): + if len(contour_textline) == 2: + points_co += str(int((contour_textline[0] + page_coord[2]) / self.scale_x)) + points_co += ',' + points_co += str(int((contour_textline[1] + page_coord[0]) / self.scale_y)) + else: + points_co += str(int((contour_textline[0][0] + page_coord[2]) / self.scale_x)) + points_co += ',' + points_co += str(int((contour_textline[0][1] + page_coord[0])/self.scale_y)) + + points_co += ' ' + coords.set_points(points_co[:-1]) def write_pagexml(self, pcgts): out_fname = os.path.join(self.dir_out, self.image_filename_stem) + ".xml" @@ -251,8 +274,12 @@ class EynollahXmlWriter(): self.serialize_lines_in_marginal(marginal, all_found_textline_polygons_marginals, mm, page_coord, all_box_coord_marginals, slopes_marginals, counter) for mm in range(len(found_polygons_drop_capitals)): - page.add_TextRegion(TextRegionType(id=counter.next_region_id, type_='drop-capital', - Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_drop_capitals[mm], page_coord)))) + dropcapital = TextRegionType(id=counter.next_region_id, type_='drop-capital', + Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_drop_capitals[mm], page_coord))) + page.add_TextRegion(dropcapital) + all_box_coord_drop = None + slopes_drop = None + self.serialize_lines_in_dropcapital(dropcapital, [found_polygons_drop_capitals[mm]], mm, page_coord, all_box_coord_drop, slopes_drop, counter, ocr_all_textlines_textregion=None) for mm in range(len(found_polygons_text_region_img)): page.add_ImageRegion(ImageRegionType(id=counter.next_region_id, Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region_img[mm], page_coord)))) From b6d3d2bdbfc206bcfaaeea67c5cbf68bed2f32b4 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Mon, 2 Sep 2024 20:11:42 +0200 Subject: [PATCH 225/412] fix indentation --- src/eynollah/eynollah.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 0081643..56036eb 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -3273,8 +3273,9 @@ class Eynollah: pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts + + if not self.dir_in: + return pcgts else: contours_only_text_parent_h = None if np.abs(slope_deskew) < SLOPE_THRESHOLD: From c3a4a1bba77d40b9be8926483e40a1ccefe42198 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 3 Sep 2024 13:14:10 +0200 Subject: [PATCH 226/412] resolving issue #110 in a better way --- qurator/eynollah/eynollah.py | 61 ++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 1bb0eff..c88f0f9 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2357,7 +2357,6 @@ class Eynollah: arg_text_con = [] for ii in range(len(cx_text_only)): for jj in range(len(boxes)): - print(cx_text_only[ii],cy_text_only[ii],'markaz') if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) break @@ -3624,6 +3623,9 @@ class Eynollah: textline_contour[:,0] = textline_contour[:,0] + box_ind[2] textline_contour[:,1] = textline_contour[:,1] + box_ind[0] return textline_contour + def return_list_of_contours_with_desired_order(self, ls_cons, sorted_indexes): + return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] + def run(self): """ @@ -3735,11 +3737,15 @@ class Eynollah: contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - try: - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - except: - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) @@ -3753,12 +3759,14 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - try: - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - except: - contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) @@ -3820,11 +3828,14 @@ class Eynollah: areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - try: - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - except: - contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + #try: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + #except: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) @@ -3865,10 +3876,11 @@ class Eynollah: #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - try: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - except: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) if self.light_version: text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: @@ -3958,10 +3970,11 @@ class Eynollah: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - try: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - except: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) From 6b2e5d110e15c7a0b5d69217a1288e4833169ade Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 3 Sep 2024 13:55:55 +0200 Subject: [PATCH 227/412] all tests are passed --- src/eynollah/eynollah.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 56036eb..caa1978 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -3271,25 +3271,25 @@ class Eynollah: else: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) - self.logger.info("Job done in %.1fs", time.time() - t0) + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml) + self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - else: - contours_only_text_parent_h = None - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + if not self.dir_in: + return pcgts else: - contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts + contours_only_text_parent_h = None + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + if self.dir_in: + self.writer.write_pagexml(pcgts) + #self.logger.info("Job done in %.1fs", time.time() - t0) if self.dir_in: - self.writer.write_pagexml(pcgts) - #self.logger.info("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) + self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From c156a1612ec8a379d209f5926e7941d5dcfe8e90 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:03:44 +0200 Subject: [PATCH 228/412] Exclude `run_image_extraction_over_ppn_lists.py` from merge --- run_image_extraction_over_ppn_lists.py | 33 -------------------------- 1 file changed, 33 deletions(-) delete mode 100644 run_image_extraction_over_ppn_lists.py diff --git a/run_image_extraction_over_ppn_lists.py b/run_image_extraction_over_ppn_lists.py deleted file mode 100644 index a890022..0000000 --- a/run_image_extraction_over_ppn_lists.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import sys - -dir_ppn = '/home/vahid/Documents/eynollah/ppn_list.txt' - - -with open(dir_ppn) as f: - ppn_list = f.readlines() - - -ppn_list = [ind.split('\n')[0] for ind in ppn_list] - -url_main = 'https://content.staatsbibliothek-berlin.de/dc/download/zip?ppn=PPN' - -out_result = './new_results_ppns2' - - -for ppn_ind in ppn_list: - url = url_main + ppn_ind - #curl -o ./ppn.zip "https://content.staatsbibliothek-berlin.de/dc/download/zip?ppn=PPN1762638355" - os.system("curl -o "+"./PPN_"+ppn_ind+".zip"+" "+url) - os.system("unzip "+"PPN_"+ppn_ind+".zip"+ " -d "+"PPN_"+ppn_ind) - os.system("rm -rf "+"PPN_"+ppn_ind+"/*.txt") - - os.system("mkdir "+out_result+'/'+"PPN_"+ppn_ind+"_out") - os.system("mkdir "+out_result+'/'+"PPN_"+ppn_ind+"_out_images") - command_eynollah = "eynollah -m /home/vahid/Downloads/models_eynollah_renamed_savedmodel -di "+"PPN_"+ppn_ind+" "+"-o "+out_result+'/'+"PPN_"+ppn_ind+"_out "+"-eoi "+"-ep -si "+out_result+'/'+"PPN_"+ppn_ind+"_out_images" - os.system(command_eynollah) - - os.system("rm -rf "+"PPN_"+ppn_ind+".zip") - os.system("rm -rf "+"PPN_"+ppn_ind) - #sys.exit() - From f0b49073b7ba4746e1facd17cf8f8598e253b1d4 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 3 Sep 2024 23:10:38 +0200 Subject: [PATCH 229/412] adding option for textline detection in printspace --- qurator/eynollah/eynollah.py | 959 +++++++++++++++++++---------------- 1 file changed, 522 insertions(+), 437 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c88f0f9..533e2a0 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -741,7 +741,7 @@ class Eynollah: return model, None - def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1): + def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False): self.logger.debug("enter do_prediction") img_height_model = model.layers[len(model.layers) - 1].output_shape[1] @@ -774,7 +774,7 @@ class Eynollah: width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin img = img / float(255.0) - img = img.astype(np.float16) + #img = img.astype(np.float16) img_h = img.shape[0] img_w = img.shape[1] prediction_true = np.zeros((img_h, img_w, 3)) @@ -832,6 +832,23 @@ class Eynollah: seg = np.argmax(label_p_pred, axis=3) + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): seg_in = seg[indexer_inside_batch,:,:] @@ -889,6 +906,22 @@ class Eynollah: label_p_pred = model.predict(img_patch,verbose=0) seg = np.argmax(label_p_pred, axis=3) + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): @@ -1202,9 +1235,9 @@ class Eynollah: img_height_h = img.shape[0] img_width_h = img.shape[1] if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully_new if patches else self.model_region_dir_fully_np) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully if patches else self.model_region_dir_fully_np) else: - model_region = self.model_region_fl_new if patches else self.model_region_fl_np + model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: if self.light_version: @@ -1809,7 +1842,7 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) - def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier): + def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier, skip_layout_ro=False): self.logger.debug("enter get_regions_light_v") t_in = time.time() erosion_hurts = False @@ -1866,89 +1899,98 @@ class Eynollah: textline_mask_tot_ea = self.run_textline(img_bin) - #print("inside 2 ", time.time()-t_in) - - #print(img_resized.shape, num_col_classifier, "num_col_classifier") - if not self.dir_in: - if num_col_classifier == 1 or num_col_classifier == 2: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) - else: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) - else: - if num_col_classifier == 1 or num_col_classifier == 2: - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) - else: - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) - - #print("inside 3 ", time.time()-t_in) - #plt.imshow(prediction_regions_org[:,:,0]) - #plt.show() - - prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) - img_bin = resize_image(img_bin,img_height_h, img_width_h ) - prediction_regions_org=prediction_regions_org[:,:,0] + if not skip_layout_ro: + #print("inside 2 ", time.time()-t_in) - mask_lines_only = (prediction_regions_org[:,:] ==3)*1 - - mask_texts_only = (prediction_regions_org[:,:] ==1)*1 - - mask_texts_only = mask_texts_only.astype('uint8') - - mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=3) - - mask_images_only=(prediction_regions_org[:,:] ==2)*1 - - polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - - - test_khat = np.zeros(prediction_regions_org.shape) - - test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) - - - #plt.imshow(test_khat[:,:]) - #plt.show() - - #for jv in range(1): - #print(jv, hir_lines_xml[0][232][3]) - #test_khat = np.zeros(prediction_regions_org.shape) + #print(img_resized.shape, num_col_classifier, "num_col_classifier") + if not self.dir_in: + ###if num_col_classifier == 1 or num_col_classifier == 2: + ###model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + ###prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) + ###else: + ###model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + ###prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) + else: + ##if num_col_classifier == 1 or num_col_classifier == 2: + ##prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) + ##else: + ##prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) + prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) - #test_khat = cv2.fillPoly(test_khat, pts = [polygons_lines_xml[232]], color=(1,1,1)) + #print("inside 3 ", time.time()-t_in) + #plt.imshow(prediction_regions_org[:,:,0]) + #plt.show() + + prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) + + img_bin = resize_image(img_bin,img_height_h, img_width_h ) + + prediction_regions_org=prediction_regions_org[:,:,0] + + mask_lines_only = (prediction_regions_org[:,:] ==3)*1 + + mask_texts_only = (prediction_regions_org[:,:] ==1)*1 + + mask_texts_only = mask_texts_only.astype('uint8') + + mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=3) + + mask_images_only=(prediction_regions_org[:,:] ==2)*1 + + polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) + + + test_khat = np.zeros(prediction_regions_org.shape) + + test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) #plt.imshow(test_khat[:,:]) #plt.show() + #for jv in range(1): + #print(jv, hir_lines_xml[0][232][3]) + #test_khat = np.zeros(prediction_regions_org.shape) + + #test_khat = cv2.fillPoly(test_khat, pts = [polygons_lines_xml[232]], color=(1,1,1)) + + + #plt.imshow(test_khat[:,:]) + #plt.show() + - polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) - - - test_khat = np.zeros(prediction_regions_org.shape) - - test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) - - - #plt.imshow(test_khat[:,:]) - #plt.show() - #sys.exit() - - polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) - - polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) - - text_regions_p_true = np.zeros(prediction_regions_org.shape) - - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) - - text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 - - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - #print("inside 4 ", time.time()-t_in) - return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin + polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + + + test_khat = np.zeros(prediction_regions_org.shape) + + test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) + + + #plt.imshow(test_khat[:,:]) + #plt.show() + #sys.exit() + + polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) + + polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) + + text_regions_p_true = np.zeros(prediction_regions_org.shape) + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) + + text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 + + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) + #print("inside 4 ", time.time()-t_in) + return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin + else: + img_bin = resize_image(img_bin,img_height_h, img_width_h ) + return None, erosion_hurts, None, textline_mask_tot_ea, img_bin def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_from_xy_2models") @@ -2392,8 +2434,6 @@ class Eynollah: ref_point += len(id_of_texts) order_of_texts_tot = [] - print(len(contours_only_text_parent),'contours_only_text_parent') - print(len(order_by_con_main),'order_by_con_main') for tj1 in range(len(contours_only_text_parent)): order_of_texts_tot.append(int(order_by_con_main[tj1])) @@ -2768,6 +2808,28 @@ class Eynollah: num_col = None #print("inside graphics 3 ", time.time() - t_in_gr) return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light + + def run_graphics_and_columns_without_layout(self, textline_mask_tot_ea, img_bin_light): + + #print(text_regions_p_1.shape, 'text_regions_p_1 shape run graphics') + #print(erosion_hurts, 'erosion_hurts') + t_in_gr = time.time() + img_g = self.imread(grayscale=True, uint8=True) + + img_g3 = np.zeros((img_g.shape[0], img_g.shape[1], 3)) + img_g3 = img_g3.astype(np.uint8) + img_g3[:, :, 0] = img_g[:, :] + img_g3[:, :, 1] = img_g[:, :] + img_g3[:, :, 2] = img_g[:, :] + + image_page, page_coord, cont_page = self.extract_page() + #print("inside graphics 1 ", time.time() - t_in_gr) + + textline_mask_tot_ea = textline_mask_tot_ea[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + + img_bin_light = img_bin_light[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + + return page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): t_in_gr = time.time() img_g = self.imread(grayscale=True, uint8=True) @@ -3632,6 +3694,8 @@ class Eynollah: Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") + + skip_layout_ro = True t0_tot = time.time() @@ -3649,398 +3713,419 @@ class Eynollah: self.logger.info("Enhancing took %.1fs ", time.time() - t0) #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) + + if not skip_layout_ro: + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) + + if num_col_classifier == 1 or num_col_classifier ==2: + if num_col_classifier == 1: + img_w_new = 1000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 1300 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + else: + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) - if num_col_classifier == 1 or num_col_classifier ==2: - if num_col_classifier == 1: - img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) - else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + #print("text region early in %.1fs", time.time() - t0) t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - #print("text region early in %.1fs", time.time() - t0) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) - - t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() - - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - - if self.full_layout: if not self.light_version: - img_bin_light = None - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - - #print("text region early 2 in %.1fs", time.time() - t0) - ###min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + t1 = time.time() + #plt.imshow(table_prediction) + #plt.show() - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) - #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - - #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) - - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + if self.full_layout: + if not self.light_version: + img_bin_light = None + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + #print("text region early 2 in %.1fs", time.time() - t0) + ###min_con_area = 0.000005 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] - else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - - index_con_parents = np.argsort(areas_cnt_text_parent) + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - #try: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - #except: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass - - #print("text region early 3 in %.1fs", time.time() - t0) - if self.light_version: - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) - else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) - if not self.curved_line: + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + #try: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + #except: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) + # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) + else: + pass + + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: - if self.textline_light: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - else: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) else: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - else: - - scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) + #print("text region early 5 in %.1fs", time.time() - t0) + if not self.curved_line: if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + if self.textline_light: + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + else: + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - - if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) - pixel_lines = 6 - - if not self.reading_order_machine_based: - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if not self.reading_order_machine_based: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - - #print(boxes_d,'boxes_d') - #img_once = np.zeros((textline_mask_tot_d.shape[0],textline_mask_tot_d.shape[1])) - #for box_i in boxes_d: - #img_once[int(box_i[2]):int(box_i[3]),int(box_i[0]):int(box_i[1]) ] =1 - #plt.imshow(img_once) - #plt.show() - #print(np.unique(img_once),'img_once') - if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() - if self.full_layout: - - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - - if self.ocr: - ocr_all_textlines = [] - else: - ocr_all_textlines = None - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - - - else: - contours_only_text_parent_h = None - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: + scale_param = 1 + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + #print("text region early 6 in %.1fs", time.time() - t0) + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - #except: #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - if self.ocr: - - device = cuda.get_current_device() - device.reset() - gc.collect() - model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") - torch.cuda.empty_cache() - model_ocr.to(device) + if self.plotter: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) + + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) + pixel_lines = 6 - ind_tot = 0 - #cv2.imwrite('./img_out.png', image_page) - - ocr_all_textlines = [] - for indexing, ind_poly_first in enumerate(all_found_textline_polygons): - ocr_textline_in_textregion = [] - for indexing2, ind_poly in enumerate(ind_poly_first): - if not (self.textline_light or self.curved_line): - ind_poly = copy.deepcopy(ind_poly) - box_ind = all_box_coord[indexing] - #print(ind_poly,np.shape(ind_poly), 'ind_poly') - #print(box_ind) - ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) - #print(ind_poly_copy) - ind_poly[ind_poly<0] = 0 - x, y, w, h = cv2.boundingRect(ind_poly) - #print(ind_poly_copy, np.shape(ind_poly_copy)) - #print(x, y, w, h, h/float(w),'ratio') - h2w_ratio = h/float(w) - mask_poly = np.zeros(image_page.shape) - if not self.light_version: - img_poly_on_img = np.copy(image_page) + if not self.reading_order_machine_based: + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) else: - img_poly_on_img = np.copy(img_bin_light) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - if self.textline_light: - mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 - - img_croped = img_poly_on_img[y:y+h, x:x+w, :] - text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) - - ocr_textline_in_textregion.append(text_ocr) + if not self.reading_order_machine_based: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() - ##cv2.imwrite(str(ind_tot)+'.png', img_croped) - ind_tot = ind_tot +1 - ocr_all_textlines.append(ocr_textline_in_textregion) + if self.full_layout: + + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + + else: - ocr_all_textlines = None - #print(ocr_all_textlines) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) + contours_only_text_parent_h = None + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + ##cv2.imwrite(str(ind_tot)+'.png', img_croped) + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) + else: + _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_ro=skip_layout_ro) + + page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) + + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) + all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + + all_found_textline_polygons=[ all_found_textline_polygons ] + order_text_new = [0] + slopes =[0] + id_of_texts_tot =['region_0001'] + + polygons_of_images = [] + slopes_marginals = [] + polygons_of_marginals = [] + all_found_textline_polygons_marginals = [] + all_box_coord_marginals = [] + polygons_lines_xml = [] + contours_tables = [] + ocr_all_textlines = None + + pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) if not self.dir_in: return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) + if self.dir_in: self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) From 2c939049854c73c7dc27e4b04863c8498d654129 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 12 Sep 2024 17:35:28 +0200 Subject: [PATCH 230/412] avoiding double binarization --- qurator/eynollah/eynollah.py | 155 +++++++++++++++++++---------- qurator/eynollah/utils/__init__.py | 4 +- 2 files changed, 106 insertions(+), 53 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 533e2a0..569aec5 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -89,7 +89,7 @@ from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter from .writer import EynollahXmlWriter -MIN_AREA_REGION = 0.00001 +MIN_AREA_REGION = 0.000001 SLOPE_THRESHOLD = 0.13 RATIO_OF_TWO_MODEL_THRESHOLD = 95.50 #98.45: DPI_THRESHOLD = 298 @@ -237,15 +237,16 @@ class Eynollah: self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" self.model_region_dir_fully_np = dir_models + "/eynollah-full-regions-1column_20210425" - self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" + #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/model_3_eraly_layout_no_patches_1_2_spaltige" - self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: - self.model_textline_dir = dir_models + "/eynollah-textline_light_20210425" + self.model_textline_dir = dir_models + "/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: self.model_textline_dir = dir_models + "/eynollah-textline_20210425" if self.ocr: @@ -267,7 +268,7 @@ class Eynollah: self.model_textline = self.our_load_model(self.model_textline_dir) self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) - self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) + ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) @@ -993,9 +994,16 @@ class Eynollah: img = resize_image(img, img_height_model, img_width_model) label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), verbose=0) - + + seg_not_base = label_p_pred[0,:,:,4] + + seg_not_base[seg_not_base>0.4] =1 + seg_not_base[seg_not_base<1] =0 seg = np.argmax(label_p_pred, axis=3)[0] + + seg[seg_not_base==1]=4 + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) prediction_true = prediction_true.astype(np.uint8) @@ -1781,7 +1789,7 @@ class Eynollah: all_box_coord_per_process.append(crop_coor) queue_of_all_params.put([slopes_per_each_subprocess, textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours]) - def textline_contours(self, img, patches, scaler_h, scaler_w): + def textline_contours(self, img, patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') if not self.dir_in: model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) @@ -1792,10 +1800,34 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) #print(img.shape,'bin shape textline') if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, n_batch_inference=3) + prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3) + if num_col_classifier==1: + prediction_textline_nopatch = self.do_prediction(False, img, model_textline) + prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, n_batch_inference=3) + prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3) + if num_col_classifier==1: + prediction_textline_nopatch = self.do_prediction(False, img, model_textline) + prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 prediction_textline = resize_image(prediction_textline, img_h, img_w) + + textline_mask_tot_ea_art = (prediction_textline[:,:]==2)*1 + + old_art = np.copy(textline_mask_tot_ea_art) + + textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') + textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) + + prediction_textline[:,:][textline_mask_tot_ea_art[:,:]==1]=2 + + textline_mask_tot_ea_lines = (prediction_textline[:,:]==1)*1 + textline_mask_tot_ea_lines = textline_mask_tot_ea_lines.astype('uint8') + textline_mask_tot_ea_lines = cv2.dilate(textline_mask_tot_ea_lines, KERNEL, iterations=1) + + prediction_textline[:,:][textline_mask_tot_ea_lines[:,:]==1]=1 + + prediction_textline[:,:][old_art[:,:]==1]=2 + if not self.dir_in: prediction_textline_longshot = self.do_prediction(False, img, model_textline) else: @@ -1855,49 +1887,58 @@ class Eynollah: #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: - img_w_new = 1000 + img_w_new = 900#1000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: - img_w_new = 1500 + img_w_new = 1300#1500 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 3: - img_w_new = 2000 + img_w_new = 1600#2000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 4: - img_w_new = 2500 + img_w_new = 1900#2500 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 5: - img_w_new = 3000 + img_w_new = 2300#3000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) else: - img_w_new = 4000 + img_w_new = 3300#4000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) img_resized = resize_image(img,img_h_new, img_w_new ) t_bin = time.time() - if not self.dir_in: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) - else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + + #if (not self.input_binary) or self.full_layout: + #if self.input_binary: + #img_bin = np.copy(img_resized) + if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 3): + if not self.dir_in: + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) + else: + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + + #print("inside bin ", time.time()-t_bin) + prediction_bin=prediction_bin[:,:,0] + prediction_bin = (prediction_bin[:,:]==0)*1 + prediction_bin = prediction_bin*255 - #print("inside bin ", time.time()-t_bin) - prediction_bin=prediction_bin[:,:,0] - prediction_bin = (prediction_bin[:,:]==0)*1 - prediction_bin = prediction_bin*255 - - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - prediction_bin = prediction_bin.astype(np.uint16) - #img= np.copy(prediction_bin) - img_bin = np.copy(prediction_bin) + prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) + + prediction_bin = prediction_bin.astype(np.uint16) + #img= np.copy(prediction_bin) + img_bin = np.copy(prediction_bin) + else: + img_bin = np.copy(img_resized) #print("inside 1 ", time.time()-t_in) - textline_mask_tot_ea = self.run_textline(img_bin) + ###textline_mask_tot_ea = self.run_textline(img_bin) + textline_mask_tot_ea = self.run_textline(img_bin, num_col_classifier) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) @@ -1906,20 +1947,20 @@ class Eynollah: #print(img_resized.shape, num_col_classifier, "num_col_classifier") if not self.dir_in: - ###if num_col_classifier == 1 or num_col_classifier == 2: - ###model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - ###prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) - ###else: - ###model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - ###prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) + if num_col_classifier == 1 or num_col_classifier == 2: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) + else: + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: - ##if num_col_classifier == 1 or num_col_classifier == 2: - ##prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) - ##else: - ##prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) - prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) + if num_col_classifier == 1 or num_col_classifier == 2: + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) + else: + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) + ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) #plt.imshow(prediction_regions_org[:,:,0]) @@ -1937,7 +1978,7 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=3) + mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=2) mask_images_only=(prediction_regions_org[:,:] ==2)*1 @@ -2899,10 +2940,11 @@ class Eynollah: #print("enhancement in ", time.time()-t_in) return img_res, is_image_enhanced, num_col_classifier, num_column_is_classified - def run_textline(self, image_page): - scaler_h_textline = 1 # 1.2#1.2 - scaler_w_textline = 1 # 0.9#1 - textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline) + def run_textline(self, image_page, num_col_classifier=None): + scaler_h_textline = 1#1.3 # 1.2#1.2 + scaler_w_textline = 1#1.3 # 0.9#1 + #print(image_page.shape) + textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline, num_col_classifier) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) @@ -3147,6 +3189,17 @@ class Eynollah: ##regions_fully_only_drop = put_drop_out_from_only_drop_model(regions_fully_only_drop, text_regions_p) ##regions_fully[:, :, 0][regions_fully_only_drop[:, :, 0] == 4] = 4 drop_capital_label_in_full_layout_model = 3 + + drops = (regions_fully[:,:,0]==drop_capital_label_in_full_layout_model)*1 + + drops= drops.astype(np.uint8) + + regions_fully[:,:,0][regions_fully[:,:,0]==drop_capital_label_in_full_layout_model] = 1 + + drops = cv2.erode(drops[:,:], KERNEL, iterations=1) + regions_fully[:,:,0][drops[:,:]==1] = drop_capital_label_in_full_layout_model + + regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: @@ -3695,7 +3748,7 @@ class Eynollah: """ self.logger.debug("enter run") - skip_layout_ro = True + skip_layout_ro = False#True t0_tot = time.time() diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 929669f..8705ecf 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -792,11 +792,11 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) - if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.4: + if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.8: layout_in_patch[y : y + h, x : x + w, 0] = drop_capital_label else: - layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = drop_capital_label + layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = 1#drop_capital_label return layout_in_patch From 1b18ae874b9ea086e99ac76281dd30572f947471 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 13 Sep 2024 00:52:06 +0200 Subject: [PATCH 231/412] passing number of columns as an argument --- qurator/eynollah/cli.py | 14 ++++- qurator/eynollah/eynollah.py | 102 ++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 25 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index b0f55cd..357582c 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -191,6 +191,16 @@ def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, i is_flag=True, help="if this parameter set to true, this tool will try to do ocr", ) +@click.option( + "--num_col_upper", + "-ncu", + help="lower limit of columns in document image", +) +@click.option( + "--num_col_lower", + "-ncl", + help="upper limit of columns in document image", +) @click.option( "--log_level", "-l", @@ -198,7 +208,7 @@ def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, i help="Override log level globally to this", ) -def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, ignore_page_extraction, log_level): +def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, ignore_page_extraction, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() @@ -235,6 +245,8 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s ignore_page_extraction=ignore_page_extraction, reading_order_machine_based=reading_order_machine_based, do_ocr=do_ocr, + num_col_upper=num_col_upper, + num_col_lower=num_col_lower, ) if dir_in: eynollah.run() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 569aec5..f76dce8 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -178,6 +178,8 @@ class Eynollah: ignore_page_extraction=False, reading_order_machine_based=False, do_ocr=False, + num_col_upper=None, + num_col_lower=None, override_dpi=None, logger=None, pcgts=None, @@ -212,6 +214,14 @@ class Eynollah: self.headers_off = headers_off self.ignore_page_extraction = ignore_page_extraction self.ocr = do_ocr + if num_col_upper: + self.num_col_upper = int(num_col_upper) + else: + self.num_col_upper = num_col_upper + if num_col_lower: + self.num_col_lower = int(num_col_lower) + else: + self.num_col_lower = num_col_lower self.pcgts = pcgts if not dir_in: self.plotter = None if not enable_plotting else EynollahPlotter( @@ -597,36 +607,80 @@ class Eynollah: else: img = self.imread() img_bin = None - + + width_early = img.shape[1] t1 = time.time() _, page_coord = self.early_page_for_num_of_column_classification(img_bin) if not self.dir_in: model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) - if self.input_binary: - img_in = np.copy(img) - width_early = img_in.shape[1] - img_in = img_in / 255.0 - img_in = cv2.resize(img_in, (448, 448), interpolation=cv2.INTER_NEAREST) - img_in = img_in.reshape(1, 448, 448, 3) + if self.num_col_upper and not self.num_col_lower: + num_col = self.num_col_upper + label_p_pred = [np.ones(6)] + elif self.num_col_lower and not self.num_col_upper: + num_col = self.num_col_lower + label_p_pred = [np.ones(6)] + + elif (not self.num_col_upper and not self.num_col_lower): + if self.input_binary: + img_in = np.copy(img) + img_in = img_in / 255.0 + img_in = cv2.resize(img_in, (448, 448), interpolation=cv2.INTER_NEAREST) + img_in = img_in.reshape(1, 448, 448, 3) + else: + img_1ch = self.imread(grayscale=True) + width_early = img_1ch.shape[1] + img_1ch = img_1ch[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + + img_1ch = img_1ch / 255.0 + img_1ch = cv2.resize(img_1ch, (448, 448), interpolation=cv2.INTER_NEAREST) + img_in = np.zeros((1, img_1ch.shape[0], img_1ch.shape[1], 3)) + img_in[0, :, :, 0] = img_1ch[:, :] + img_in[0, :, :, 1] = img_1ch[:, :] + img_in[0, :, :, 2] = img_1ch[:, :] + + + if self.dir_in: + label_p_pred = self.model_classifier.predict(img_in, verbose=0) + else: + label_p_pred = model_num_classifier.predict(img_in, verbose=0) + num_col = np.argmax(label_p_pred[0]) + 1 + elif (self.num_col_upper and self.num_col_lower) and (self.num_col_upper!=self.num_col_lower): + if self.input_binary: + img_in = np.copy(img) + img_in = img_in / 255.0 + img_in = cv2.resize(img_in, (448, 448), interpolation=cv2.INTER_NEAREST) + img_in = img_in.reshape(1, 448, 448, 3) + else: + img_1ch = self.imread(grayscale=True) + width_early = img_1ch.shape[1] + img_1ch = img_1ch[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] + + img_1ch = img_1ch / 255.0 + img_1ch = cv2.resize(img_1ch, (448, 448), interpolation=cv2.INTER_NEAREST) + img_in = np.zeros((1, img_1ch.shape[0], img_1ch.shape[1], 3)) + img_in[0, :, :, 0] = img_1ch[:, :] + img_in[0, :, :, 1] = img_1ch[:, :] + img_in[0, :, :, 2] = img_1ch[:, :] + + + if self.dir_in: + label_p_pred = self.model_classifier.predict(img_in, verbose=0) + else: + label_p_pred = model_num_classifier.predict(img_in, verbose=0) + num_col = np.argmax(label_p_pred[0]) + 1 + + if num_col > self.num_col_upper: + num_col = self.num_col_upper + label_p_pred = [np.ones(6)] + if num_col < self.num_col_lower: + num_col = self.num_col_lower + label_p_pred = [np.ones(6)] + else: - img_1ch = self.imread(grayscale=True) - width_early = img_1ch.shape[1] - img_1ch = img_1ch[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] - - img_1ch = img_1ch / 255.0 - img_1ch = cv2.resize(img_1ch, (448, 448), interpolation=cv2.INTER_NEAREST) - img_in = np.zeros((1, img_1ch.shape[0], img_1ch.shape[1], 3)) - img_in[0, :, :, 0] = img_1ch[:, :] - img_in[0, :, :, 1] = img_1ch[:, :] - img_in[0, :, :, 2] = img_1ch[:, :] - - - if self.dir_in: - label_p_pred = self.model_classifier.predict(img_in, verbose=0) - else: - label_p_pred = model_num_classifier.predict(img_in, verbose=0) - num_col = np.argmax(label_p_pred[0]) + 1 + num_col = self.num_col_upper + label_p_pred = [np.ones(6)] + self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) From 478edc804a0de01a1966a0df24468344e7a26cf0 Mon Sep 17 00:00:00 2001 From: kba Date: Mon, 16 Sep 2024 18:21:14 +0200 Subject: [PATCH 232/412] Add Dockerfile and make docker target --- Dockerfile | 26 ++++++++++++++++++++++++++ Makefile | 14 ++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c76564 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +ARG DOCKER_BASE_IMAGE +FROM $DOCKER_BASE_IMAGE + +ARG VCS_REF +ARG BUILD_DATE +LABEL \ + maintainer="https://ocr-d.de/kontakt" \ + org.label-schema.vcs-ref=$VCS_REF \ + org.label-schema.vcs-url="https://github.com/qurator-spk/eynollah" \ + org.label-schema.build-date=$BUILD_DATE + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONIOENCODING=utf8 +ENV XDG_DATA_HOME=/usr/local/share + +WORKDIR /build-eynollah +COPY qurator/ ./qurator +COPY pyproject.toml . +COPY requirements.txt . +COPY README.md . +COPY Makefile . +RUN apt-get install -y --no-install-recommends g++ +RUN make install + +WORKDIR /data +VOLUME /data diff --git a/Makefile b/Makefile index 4b43564..a3bde05 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ EYNOLLAH_MODELS ?= $(PWD)/models_eynollah export EYNOLLAH_MODELS +# DOCKER_BASE_IMAGE = artefakt.dev.sbb.berlin:5000/sbb/ocrd_core:v2.68.0 +DOCKER_BASE_IMAGE = docker.io/ocrd/core:v2.68.0 +DOCKER_TAG = ocrd/eynollah + + # BEGIN-EVAL makefile-parser --make-help Makefile help: @@ -48,3 +53,12 @@ smoke-test: # Run unit tests test: pytest tests + +# Build docker image +docker: + docker build \ + --build-arg DOCKER_BASE_IMAGE=$(DOCKER_BASE_IMAGE) \ + --build-arg VCS_REF=$$(git rev-parse --short HEAD) \ + --build-arg BUILD_DATE=$$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + -t $(DOCKER_TAG) . + From 21380fc8706474f0c6c791560fb6a5174d03aa8e Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 17 Sep 2024 15:06:41 +0200 Subject: [PATCH 233/412] scaling contours without dilation --- qurator/eynollah/eynollah.py | 207 +++++++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 23 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index f76dce8..79cf98b 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -256,7 +256,7 @@ class Eynollah: ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: - self.model_textline_dir = dir_models + "/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: self.model_textline_dir = dir_models + "/eynollah-textline_20210425" if self.ocr: @@ -796,7 +796,7 @@ class Eynollah: return model, None - def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False): + def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction") img_height_model = model.layers[len(model.layers) - 1].output_shape[1] @@ -903,6 +903,13 @@ class Eynollah: seg[seg_not_base==1]=4 seg[seg_background==1]=0 seg[(seg_line==1) & (seg==0)]=3 + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): @@ -977,6 +984,14 @@ class Eynollah: seg[seg_not_base==1]=4 seg[seg_background==1]=0 seg[(seg_line==1) & (seg==0)]=3 + + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): @@ -1845,42 +1860,50 @@ class Eynollah: def textline_contours(self, img, patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') + thresholding_for_artificial_class_in_light_version = True#False if not self.dir_in: model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) - img = img.astype(np.uint8) + #img = img.astype(np.uint8) img_org = np.copy(img) img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - #print(img.shape,'bin shape textline') + if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3) - if num_col_classifier==1: - prediction_textline_nopatch = self.do_prediction(False, img, model_textline) - prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 + prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + + #if not thresholding_for_artificial_class_in_light_version: + #if num_col_classifier==1: + #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) + #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3) - if num_col_classifier==1: - prediction_textline_nopatch = self.do_prediction(False, img, model_textline) - prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 + prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + #if not thresholding_for_artificial_class_in_light_version: + #if num_col_classifier==1: + #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) + #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 prediction_textline = resize_image(prediction_textline, img_h, img_w) textline_mask_tot_ea_art = (prediction_textline[:,:]==2)*1 old_art = np.copy(textline_mask_tot_ea_art) - textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') - textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) - - prediction_textline[:,:][textline_mask_tot_ea_art[:,:]==1]=2 + if not thresholding_for_artificial_class_in_light_version: + textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') + textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) + + prediction_textline[:,:][textline_mask_tot_ea_art[:,:]==1]=2 textline_mask_tot_ea_lines = (prediction_textline[:,:]==1)*1 textline_mask_tot_ea_lines = textline_mask_tot_ea_lines.astype('uint8') - textline_mask_tot_ea_lines = cv2.dilate(textline_mask_tot_ea_lines, KERNEL, iterations=1) + + if not thresholding_for_artificial_class_in_light_version: + textline_mask_tot_ea_lines = cv2.dilate(textline_mask_tot_ea_lines, KERNEL, iterations=1) prediction_textline[:,:][textline_mask_tot_ea_lines[:,:]==1]=1 - prediction_textline[:,:][old_art[:,:]==1]=2 + if not thresholding_for_artificial_class_in_light_version: + prediction_textline[:,:][old_art[:,:]==1]=2 if not self.dir_in: prediction_textline_longshot = self.do_prediction(False, img, model_textline) @@ -1959,7 +1982,7 @@ class Eynollah: img_w_new = 2300#3000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) else: - img_w_new = 3300#4000 + img_w_new = 3000#4000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) img_resized = resize_image(img,img_h_new, img_w_new ) @@ -1968,7 +1991,7 @@ class Eynollah: #if (not self.input_binary) or self.full_layout: #if self.input_binary: #img_bin = np.copy(img_resized) - if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 3): + if (not self.input_binary and self.full_layout):# or (not self.input_binary and num_col_classifier >= 3): if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) @@ -3794,15 +3817,146 @@ class Eynollah: return textline_contour def return_list_of_contours_with_desired_order(self, ls_cons, sorted_indexes): return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] - + + def scale_contours(self,all_found_textline_polygons): + for i in range(len(all_found_textline_polygons[0])): + con_ind = all_found_textline_polygons[0][i] + x_min = np.min( con_ind[:,0,0] ) + y_min = np.min( con_ind[:,0,1] ) + + x_max = np.max( con_ind[:,0,0] ) + y_max = np.max( con_ind[:,0,1] ) + + x_mean = np.mean( con_ind[:,0,0] ) + y_mean = np.mean( con_ind[:,0,1] ) + + arg_y_max = np.argmax( con_ind[:,0,1] ) + arg_y_min = np.argmin( con_ind[:,0,1] ) + + x_cor_y_max = con_ind[arg_y_max,0,0] + x_cor_y_min = con_ind[arg_y_min,0,0] + + m_con = (y_max - y_min) / float(x_cor_y_max - x_cor_y_min) + + con_scaled = con_ind*1 + + con_scaled = con_scaled.astype(np.float) + + con_scaled[:,0,0] = con_scaled[:,0,0] - int(x_mean) + con_scaled[:,0,1] = con_scaled[:,0,1] - int(y_mean) + + if (x_max - x_min) > (y_max - y_min): + + if (y_max-y_min)<=15: + con_scaled[:,0,1] = con_ind[:,0,1]*1.8 + + y_max_scaled = np.max(con_scaled[:,0,1]) + y_min_scaled = np.min(con_scaled[:,0,1]) + + y_max_expected = ( m_con*1.8*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) + elif (y_max-y_min)<=30 and (y_max-y_min)>15: + con_scaled[:,0,1] = con_ind[:,0,1]*1.6 + y_max_scaled = np.max(con_scaled[:,0,1]) + y_min_scaled = np.min(con_scaled[:,0,1]) + + y_max_expected = ( m_con*1.6*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) + elif (y_max-y_min)>30 and (y_max-y_min)<100: + con_scaled[:,0,1] = con_ind[:,0,1]*1.35 + y_max_scaled = np.max(con_scaled[:,0,1]) + y_min_scaled = np.min(con_scaled[:,0,1]) + + y_max_expected = ( m_con*1.35*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) + else: + con_scaled[:,0,1] = con_ind[:,0,1]*1.2 + y_max_scaled = np.max(con_scaled[:,0,1]) + y_min_scaled = np.min(con_scaled[:,0,1]) + + y_max_expected = ( m_con*1.2*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) + con_scaled[:,0,0] = con_ind[:,0,0]*1.03 + + + + + if y_max_expected<=y_max_scaled: + con_scaled[:,0,1] = con_scaled[:,0,1] - y_min_scaled + + con_scaled[:,0,1] = con_scaled[:,0,1]*(y_max_expected - y_min_scaled)/ (y_max_scaled - y_min_scaled) + con_scaled[:,0,1] = con_scaled[:,0,1] + y_min_scaled + + else: + + if (x_max-x_min)<=15: + con_scaled[:,0,0] = con_ind[:,0,0]*1.8 + elif (x_max-x_min)<=30 and (x_max-x_min)>15: + con_scaled[:,0,0] = con_ind[:,0,0]*1.6 + elif (x_max-x_min)>30 and (x_max-x_min)<100: + con_scaled[:,0,0] = con_ind[:,0,0]*1.35 + else: + con_scaled[:,0,0] = con_ind[:,0,0]*1.2 + con_scaled[:,0,1] = con_ind[:,0,1]*1.03 + + + x_min_n = np.min( con_scaled[:,0,0] ) + y_min_n = np.min( con_scaled[:,0,1] ) + + x_mean_n = np.mean( con_scaled[:,0,0] ) + y_mean_n = np.mean( con_scaled[:,0,1] ) + + ##diff_x = (x_min_n - x_min)*1 + ##diff_y = (y_min_n - y_min)*1 + + diff_x = (x_mean_n - x_mean)*1 + diff_y = (y_mean_n - y_mean)*1 + + + con_scaled[:,0,0] = (con_scaled[:,0,0] - diff_x) + con_scaled[:,0,1] = (con_scaled[:,0,1] - diff_y) + + x_max_n = np.max( con_scaled[:,0,0] ) + y_max_n = np.max( con_scaled[:,0,1] ) + + diff_disp_x = (x_max_n - x_max) / 2. + diff_disp_y = (y_max_n - y_max) / 2. + + x_vals = np.array( np.abs(con_scaled[:,0,0] - diff_disp_x) ).astype(np.int16) + y_vals = np.array( np.abs(con_scaled[:,0,1] - diff_disp_y) ).astype(np.int16) + all_found_textline_polygons[0][i][:,0,0] = x_vals[:] + all_found_textline_polygons[0][i][:,0,1] = y_vals[:] + return all_found_textline_polygons + + def scale_contours_new(self, textline_mask_tot_ea): + + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) + all_found_textline_polygons1 = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + + + textline_mask_tot_ea_res = resize_image(textline_mask_tot_ea, int( textline_mask_tot_ea.shape[0]*1.6), textline_mask_tot_ea.shape[1]) + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea_res) + ##all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea_res, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea_res, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + + for i in range(len(all_found_textline_polygons)): + + #x_mean_1 = np.mean( all_found_textline_polygons1[i][:,0,0] ) + y_mean_1 = np.mean( all_found_textline_polygons1[i][:,0,1] ) + + #x_mean = np.mean( all_found_textline_polygons[i][:,0,0] ) + y_mean = np.mean( all_found_textline_polygons[i][:,0,1] ) + + ydiff = y_mean - y_mean_1 + + all_found_textline_polygons[i][:,0,1] = all_found_textline_polygons[i][:,0,1] - ydiff + return all_found_textline_polygons + + def run(self): """ Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - skip_layout_ro = False#True + skip_layout_ro = True t0_tot = time.time() @@ -3820,7 +3974,6 @@ class Eynollah: self.logger.info("Enhancing took %.1fs ", time.time() - t0) #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() - if not skip_layout_ro: if self.light_version: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) @@ -4032,6 +4185,7 @@ class Eynollah: if self.textline_light: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + else: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) @@ -4212,10 +4366,17 @@ class Eynollah: page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) + + ##all_found_textline_polygons =self.scale_contours_new(textline_mask_tot_ea) + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) all_found_textline_polygons=[ all_found_textline_polygons ] + + all_found_textline_polygons = self.scale_contours(all_found_textline_polygons) + + order_text_new = [0] slopes =[0] id_of_texts_tot =['region_0001'] From 351e9a897a390cc5978346ae56bd725f021876d9 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:32:23 +0200 Subject: [PATCH 234/412] update `ocrd-tool.json` with v0.3.1 models --- src/eynollah/ocrd-tool.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json index 4551168..b840005 100644 --- a/src/eynollah/ocrd-tool.json +++ b/src/eynollah/ocrd-tool.json @@ -52,10 +52,10 @@ }, "resources": [ { - "description": "models for eynollah (TensorFlow format)", - "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz", + "description": "models for eynollah (TensorFlow SavedModel format)", + "url": "https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz", "name": "default", - "size": 1761991295, + "size": 1894627041, "type": "archive", "path_in_archive": "models_eynollah" } From 327b446a16cc0d28281d41c96de1062c18293601 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:39:17 +0200 Subject: [PATCH 235/412] update Makefile with v0.3.1 models --- Makefile | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4b43564..e0ff6a9 100644 --- a/Makefile +++ b/Makefile @@ -22,17 +22,14 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - # tar xf models_eynollah_renamed.tar.gz --transform 's/models_eynollah_renamed/models_eynollah/' - # tar xf models_eynollah_renamed.tar.gz - # tar xf models_eynollah_renamed_savedmodel.tar.gz --transform 's/models_eynollah_renamed_savedmodel/models_eynollah/' tar xf models_eynollah.tar.gz models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - # wget 'https://ocr-d.kba.cloud/2022-04-05.SavedModel.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' - wget https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz + # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' + wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' # Install with pip install: From a1f1f98de3ad7500c80bb5d183fc86aa66e031e5 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 18 Sep 2024 00:08:54 +0200 Subject: [PATCH 236/412] updating scaling contours --- qurator/eynollah/eynollah.py | 82 ++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 79cf98b..bbfba0f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3821,23 +3821,51 @@ class Eynollah: def scale_contours(self,all_found_textline_polygons): for i in range(len(all_found_textline_polygons[0])): con_ind = all_found_textline_polygons[0][i] - x_min = np.min( con_ind[:,0,0] ) - y_min = np.min( con_ind[:,0,1] ) - x_max = np.max( con_ind[:,0,0] ) - y_max = np.max( con_ind[:,0,1] ) + con_ind = con_ind.astype(np.float) + x_differential = np.diff( con_ind[:,0,0]) + y_differential = np.diff( con_ind[:,0,1]) - x_mean = np.mean( con_ind[:,0,0] ) - y_mean = np.mean( con_ind[:,0,1] ) + + m_arr = y_differential / x_differential + + #print(x_differential, 'x_differential') + + #print(y_differential, 'y_differential') + + #print(m_arr) + + x_min = float(np.min( con_ind[:,0,0] )) + y_min = float(np.min( con_ind[:,0,1] )) + + x_max = float(np.max( con_ind[:,0,0] )) + y_max = float(np.max( con_ind[:,0,1] )) + + x_mean = float(np.mean( con_ind[:,0,0] )) + y_mean = float(np.mean( con_ind[:,0,1] )) arg_y_max = np.argmax( con_ind[:,0,1] ) arg_y_min = np.argmin( con_ind[:,0,1] ) - x_cor_y_max = con_ind[arg_y_max,0,0] - x_cor_y_min = con_ind[arg_y_min,0,0] - m_con = (y_max - y_min) / float(x_cor_y_max - x_cor_y_min) + arg_x_max = np.argmax( con_ind[:,0,0] ) + arg_x_min = np.argmin( con_ind[:,0,0] ) + x_cor_y_max = float(con_ind[arg_y_max,0,0]) + x_cor_y_min = float(con_ind[arg_y_min,0,0]) + + + y_cor_x_max = float(con_ind[arg_x_max,0,1]) + y_cor_x_min = float(con_ind[arg_x_min,0,1]) + + if (x_cor_y_max - x_cor_y_min) != 0: + m_con = (y_max - y_min) / (x_cor_y_max - x_cor_y_min) + else: + m_con= None + + + m_con_x = (x_max - x_min) / (y_cor_x_max - y_cor_x_min) + #print(m_con,m_con_x, 'm_con') con_scaled = con_ind*1 con_scaled = con_scaled.astype(np.float) @@ -3845,7 +3873,6 @@ class Eynollah: con_scaled[:,0,0] = con_scaled[:,0,0] - int(x_mean) con_scaled[:,0,1] = con_scaled[:,0,1] - int(y_mean) - if (x_max - x_min) > (y_max - y_min): if (y_max-y_min)<=15: @@ -3877,7 +3904,7 @@ class Eynollah: - + #print(m_con, (x_cor_y_max-x_cor_y_min),y_min_scaled, y_max_expected, y_max_scaled, "y_max_scaled") if y_max_expected<=y_max_scaled: con_scaled[:,0,1] = con_scaled[:,0,1] - y_min_scaled @@ -3885,17 +3912,48 @@ class Eynollah: con_scaled[:,0,1] = con_scaled[:,0,1] + y_min_scaled else: - + #print(x_max-x_min, m_con_x,'m_con_x') if (x_max-x_min)<=15: con_scaled[:,0,0] = con_ind[:,0,0]*1.8 + + x_max_scaled = np.max(con_scaled[:,0,0]) + x_min_scaled = np.min(con_scaled[:,0,0]) + + x_max_expected = ( m_con_x*1.8*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) + elif (x_max-x_min)<=30 and (x_max-x_min)>15: con_scaled[:,0,0] = con_ind[:,0,0]*1.6 + + x_max_scaled = np.max(con_scaled[:,0,0]) + x_min_scaled = np.min(con_scaled[:,0,0]) + + x_max_expected = ( m_con_x*1.6*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) + elif (x_max-x_min)>30 and (x_max-x_min)<100: con_scaled[:,0,0] = con_ind[:,0,0]*1.35 + + x_max_scaled = np.max(con_scaled[:,0,0]) + x_min_scaled = np.min(con_scaled[:,0,0]) + + x_max_expected = ( m_con_x*1.35*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) + else: con_scaled[:,0,0] = con_ind[:,0,0]*1.2 + + x_max_scaled = np.max(con_scaled[:,0,0]) + x_min_scaled = np.min(con_scaled[:,0,0]) + + x_max_expected = ( m_con_x*1.2*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) + con_scaled[:,0,1] = con_ind[:,0,1]*1.03 + #print(x_max_expected, x_max_scaled, "x_max_scaled") + if x_max_expected<=x_max_scaled: + con_scaled[:,0,0] = con_scaled[:,0,0] - x_min_scaled + + con_scaled[:,0,0] = con_scaled[:,0,0]*(x_max_expected - x_min_scaled)/ (x_max_scaled - x_min_scaled) + con_scaled[:,0,0] = con_scaled[:,0,0] + x_min_scaled + x_min_n = np.min( con_scaled[:,0,0] ) y_min_n = np.min( con_scaled[:,0,1] ) From 74a0699f6bd441315e20223da81851ef1be53121 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 19 Sep 2024 11:20:13 +0200 Subject: [PATCH 237/412] extracting images only now works for a single image input --- src/eynollah/eynollah.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index caa1978..511e994 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -3013,10 +3013,11 @@ class Eynollah: if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) - #plt.imshow(text_regions_p_1) - #plt.show() - self.writer.write_pagexml(pcgts) + if self.dir_in: + self.writer.write_pagexml(pcgts) + else: + return pcgts else: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) From 723f27bec44104632bc62bce39f59f44cb6be97a Mon Sep 17 00:00:00 2001 From: michalbubula <149780022+michalbubula@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:41:17 +0200 Subject: [PATCH 238/412] Add -eoi option to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1720f7f..292cfbc 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ The following options can be used to further configure the processing: | `-cl` | apply contour detection for curved text lines instead of bounding boxes | | `-ib` | apply binarization (the resulting image is saved to the output directory) | | `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | +| `-eoi` | extract only images to output directory (other processing will not be done) | | `-ho` | ignore headers for reading order dectection | | `-si ` | save image regions detected to this directory | | `-sd ` | save deskewed image to this directory | From d168edfd77119fb9501cf66aaa0f5f42a687f248 Mon Sep 17 00:00:00 2001 From: michalbubula <149780022+michalbubula@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:20:37 +0200 Subject: [PATCH 239/412] Update cli.py to block other processing in the case of extract_image_only --- src/eynollah/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index 82505ed..564b8b0 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -182,8 +182,8 @@ def main( if textline_light and not light_version: print('Error: You used -tll to enable light textline detection but -light is not enabled') sys.exit(1) - if extract_only_images and (allow_enhancement or allow_scaling or light_version) : - print('Error: You used -eoi which can not be enabled alongside light_version -light or allow_scaling -as or allow_enhancement -ae') + if extract_only_images and (allow_enhancement or allow_scaling or light_version or curved_line or textline_light or full_layout or tables or right2left or headers_off) : + print('Error: You used -eoi which can not be enabled alongside light_version -light or allow_scaling -as or allow_enhancement -ae or curved_line -cl or textline_light -tll or full_layout -fl or tables -tab or right2left -r2l or headers_off -ho') sys.exit(1) eynollah = Eynollah( image_filename=image, From 5a07cd9cfa9713e8944195fff6416ed6e639c121 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 19 Sep 2024 16:21:55 +0200 Subject: [PATCH 240/412] the most effective version of contours dilation without opencv and all at once --- qurator/eynollah/eynollah.py | 274 ++++++++++++++--------------------- 1 file changed, 105 insertions(+), 169 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index bbfba0f..cb70107 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1964,7 +1964,7 @@ class Eynollah: #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: - img_w_new = 900#1000 + img_w_new = 800#1000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: @@ -3818,196 +3818,132 @@ class Eynollah: def return_list_of_contours_with_desired_order(self, ls_cons, sorted_indexes): return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] - def scale_contours(self,all_found_textline_polygons): + def dilate_textlines(self,all_found_textline_polygons): for i in range(len(all_found_textline_polygons[0])): con_ind = all_found_textline_polygons[0][i] con_ind = con_ind.astype(np.float) + x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) - - m_arr = y_differential / x_differential - - #print(x_differential, 'x_differential') - - #print(y_differential, 'y_differential') - - #print(m_arr) - x_min = float(np.min( con_ind[:,0,0] )) y_min = float(np.min( con_ind[:,0,1] )) x_max = float(np.max( con_ind[:,0,0] )) y_max = float(np.max( con_ind[:,0,1] )) - - x_mean = float(np.mean( con_ind[:,0,0] )) - y_mean = float(np.mean( con_ind[:,0,1] )) - - arg_y_max = np.argmax( con_ind[:,0,1] ) - arg_y_min = np.argmin( con_ind[:,0,1] ) - - - arg_x_max = np.argmax( con_ind[:,0,0] ) - arg_x_min = np.argmin( con_ind[:,0,0] ) - - x_cor_y_max = float(con_ind[arg_y_max,0,0]) - x_cor_y_min = float(con_ind[arg_y_min,0,0]) - - - y_cor_x_max = float(con_ind[arg_x_max,0,1]) - y_cor_x_min = float(con_ind[arg_x_min,0,1]) - - if (x_cor_y_max - x_cor_y_min) != 0: - m_con = (y_max - y_min) / (x_cor_y_max - x_cor_y_min) - else: - m_con= None - - - m_con_x = (x_max - x_min) / (y_cor_x_max - y_cor_x_min) - #print(m_con,m_con_x, 'm_con') - con_scaled = con_ind*1 - - con_scaled = con_scaled.astype(np.float) - - con_scaled[:,0,0] = con_scaled[:,0,0] - int(x_mean) - con_scaled[:,0,1] = con_scaled[:,0,1] - int(y_mean) - - if (x_max - x_min) > (y_max - y_min): - - if (y_max-y_min)<=15: - con_scaled[:,0,1] = con_ind[:,0,1]*1.8 - - y_max_scaled = np.max(con_scaled[:,0,1]) - y_min_scaled = np.min(con_scaled[:,0,1]) - - y_max_expected = ( m_con*1.8*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) - elif (y_max-y_min)<=30 and (y_max-y_min)>15: - con_scaled[:,0,1] = con_ind[:,0,1]*1.6 - y_max_scaled = np.max(con_scaled[:,0,1]) - y_min_scaled = np.min(con_scaled[:,0,1]) - - y_max_expected = ( m_con*1.6*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) - elif (y_max-y_min)>30 and (y_max-y_min)<100: - con_scaled[:,0,1] = con_ind[:,0,1]*1.35 - y_max_scaled = np.max(con_scaled[:,0,1]) - y_min_scaled = np.min(con_scaled[:,0,1]) - - y_max_expected = ( m_con*1.35*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) - else: - con_scaled[:,0,1] = con_ind[:,0,1]*1.2 - y_max_scaled = np.max(con_scaled[:,0,1]) - y_min_scaled = np.min(con_scaled[:,0,1]) - - y_max_expected = ( m_con*1.2*(x_cor_y_max-x_cor_y_min) + y_min_scaled ) - con_scaled[:,0,0] = con_ind[:,0,0]*1.03 - + + if (y_max - y_min) > (x_max - x_min) and (x_max - x_min)<70: - #print(m_con, (x_cor_y_max-x_cor_y_min),y_min_scaled, y_max_expected, y_max_scaled, "y_max_scaled") - if y_max_expected<=y_max_scaled: - con_scaled[:,0,1] = con_scaled[:,0,1] - y_min_scaled + x_biger_than_x = np.abs(x_differential) > np.abs(y_differential) + + mult = x_biger_than_x*x_differential + + arg_min_mult = np.argmin(mult) + arg_max_mult = np.argmax(mult) + + if y_differential[0]==0: + y_differential[0] = 0.1 + + if y_differential[-1]==0: + y_differential[-1]= 0.1 - con_scaled[:,0,1] = con_scaled[:,0,1]*(y_max_expected - y_min_scaled)/ (y_max_scaled - y_min_scaled) - con_scaled[:,0,1] = con_scaled[:,0,1] + y_min_scaled + + + y_differential = [y_differential[ind] if y_differential[ind]!=0 else (y_differential[ind-1] + y_differential[ind+1])/2. for ind in range(len(y_differential)) ] + + if y_differential[0]==0.1: + y_differential[0] = y_differential[1] + if y_differential[-1]==0.1: + y_differential[-1] = y_differential[-2] + + y_differential.append(y_differential[0]) + + y_differential = [-1 if y_differential[ind]<0 else 1 for ind in range(len(y_differential))] + + y_differential = np.array(y_differential) + + + con_scaled = con_ind*1 + + con_scaled[:,0, 0] = con_ind[:,0,0] - 8*y_differential + + con_scaled[arg_min_mult,0, 1] = con_ind[arg_min_mult,0,1] + 8 + con_scaled[arg_min_mult+1,0, 1] = con_ind[arg_min_mult+1,0,1] + 8 + + try: + con_scaled[arg_min_mult-1,0, 1] = con_ind[arg_min_mult-1,0,1] + 5 + con_scaled[arg_min_mult+2,0, 1] = con_ind[arg_min_mult+2,0,1] + 5 + except: + pass + + con_scaled[arg_max_mult,0, 1] = con_ind[arg_max_mult,0,1] - 8 + con_scaled[arg_max_mult+1,0, 1] = con_ind[arg_max_mult+1,0,1] - 8 + + try: + con_scaled[arg_max_mult-1,0, 1] = con_ind[arg_max_mult-1,0,1] - 5 + con_scaled[arg_max_mult+2,0, 1] = con_ind[arg_max_mult+2,0,1] - 5 + except: + pass + + else: - #print(x_max-x_min, m_con_x,'m_con_x') - if (x_max-x_min)<=15: - con_scaled[:,0,0] = con_ind[:,0,0]*1.8 - - x_max_scaled = np.max(con_scaled[:,0,0]) - x_min_scaled = np.min(con_scaled[:,0,0]) - - x_max_expected = ( m_con_x*1.8*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) - - elif (x_max-x_min)<=30 and (x_max-x_min)>15: - con_scaled[:,0,0] = con_ind[:,0,0]*1.6 - - x_max_scaled = np.max(con_scaled[:,0,0]) - x_min_scaled = np.min(con_scaled[:,0,0]) - - x_max_expected = ( m_con_x*1.6*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) - - elif (x_max-x_min)>30 and (x_max-x_min)<100: - con_scaled[:,0,0] = con_ind[:,0,0]*1.35 - - x_max_scaled = np.max(con_scaled[:,0,0]) - x_min_scaled = np.min(con_scaled[:,0,0]) - - x_max_expected = ( m_con_x*1.35*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) - - else: - con_scaled[:,0,0] = con_ind[:,0,0]*1.2 - - x_max_scaled = np.max(con_scaled[:,0,0]) - x_min_scaled = np.min(con_scaled[:,0,0]) - - x_max_expected = ( m_con_x*1.2*(y_cor_x_max-y_cor_x_min) + x_min_scaled ) - - con_scaled[:,0,1] = con_ind[:,0,1]*1.03 + + y_biger_than_x = np.abs(y_differential) > np.abs(x_differential) - #print(x_max_expected, x_max_scaled, "x_max_scaled") - if x_max_expected<=x_max_scaled: - con_scaled[:,0,0] = con_scaled[:,0,0] - x_min_scaled - - con_scaled[:,0,0] = con_scaled[:,0,0]*(x_max_expected - x_min_scaled)/ (x_max_scaled - x_min_scaled) - con_scaled[:,0,0] = con_scaled[:,0,0] + x_min_scaled + mult = y_biger_than_x*y_differential + + arg_min_mult = np.argmin(mult) + arg_max_mult = np.argmax(mult) + + if x_differential[0]==0: + x_differential[0] = 0.1 + + if x_differential[-1]==0: + x_differential[-1]= 0.1 + + + + x_differential = [x_differential[ind] if x_differential[ind]!=0 else (x_differential[ind-1] + x_differential[ind+1])/2. for ind in range(len(x_differential)) ] + + + if x_differential[0]==0.1: + x_differential[0] = x_differential[1] + if x_differential[-1]==0.1: + x_differential[-1] = x_differential[-2] + + x_differential.append(x_differential[0]) + + x_differential = [-1 if x_differential[ind]<0 else 1 for ind in range(len(x_differential))] + + x_differential = np.array(x_differential) + + con_scaled = con_ind*1 + + con_scaled[:,0, 1] = con_ind[:,0,1] + 8*x_differential + + con_scaled[arg_min_mult,0, 0] = con_ind[arg_min_mult,0,0] + 8 + con_scaled[arg_min_mult+1,0, 0] = con_ind[arg_min_mult+1,0,0] + 8 + + con_scaled[arg_min_mult-1,0, 0] = con_ind[arg_min_mult-1,0,0] + 5 + con_scaled[arg_min_mult+2,0, 0] = con_ind[arg_min_mult+2,0,0] + 5 + + con_scaled[arg_max_mult,0, 0] = con_ind[arg_max_mult,0,0] - 8 + con_scaled[arg_max_mult+1,0, 0] = con_ind[arg_max_mult+1,0,0] - 8 + + con_scaled[arg_max_mult-1,0, 0] = con_ind[arg_max_mult-1,0,0] - 5 + con_scaled[arg_max_mult+2,0, 0] = con_ind[arg_max_mult+2,0,0] - 5 - - x_min_n = np.min( con_scaled[:,0,0] ) - y_min_n = np.min( con_scaled[:,0,1] ) - x_mean_n = np.mean( con_scaled[:,0,0] ) - y_mean_n = np.mean( con_scaled[:,0,1] ) - - ##diff_x = (x_min_n - x_min)*1 - ##diff_y = (y_min_n - y_min)*1 + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 + con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 - diff_x = (x_mean_n - x_mean)*1 - diff_y = (y_mean_n - y_mean)*1 + all_found_textline_polygons[0][i][:,0,1] = con_scaled[:,0, 1] + all_found_textline_polygons[0][i][:,0,0] = con_scaled[:,0, 0] - - con_scaled[:,0,0] = (con_scaled[:,0,0] - diff_x) - con_scaled[:,0,1] = (con_scaled[:,0,1] - diff_y) - - x_max_n = np.max( con_scaled[:,0,0] ) - y_max_n = np.max( con_scaled[:,0,1] ) - - diff_disp_x = (x_max_n - x_max) / 2. - diff_disp_y = (y_max_n - y_max) / 2. - - x_vals = np.array( np.abs(con_scaled[:,0,0] - diff_disp_x) ).astype(np.int16) - y_vals = np.array( np.abs(con_scaled[:,0,1] - diff_disp_y) ).astype(np.int16) - all_found_textline_polygons[0][i][:,0,0] = x_vals[:] - all_found_textline_polygons[0][i][:,0,1] = y_vals[:] return all_found_textline_polygons - - def scale_contours_new(self, textline_mask_tot_ea): - - cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) - all_found_textline_polygons1 = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) - - - textline_mask_tot_ea_res = resize_image(textline_mask_tot_ea, int( textline_mask_tot_ea.shape[0]*1.6), textline_mask_tot_ea.shape[1]) - cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea_res) - ##all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea_res, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) - all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea_res, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) - - for i in range(len(all_found_textline_polygons)): - - #x_mean_1 = np.mean( all_found_textline_polygons1[i][:,0,0] ) - y_mean_1 = np.mean( all_found_textline_polygons1[i][:,0,1] ) - - #x_mean = np.mean( all_found_textline_polygons[i][:,0,0] ) - y_mean = np.mean( all_found_textline_polygons[i][:,0,1] ) - - ydiff = y_mean - y_mean_1 - - all_found_textline_polygons[i][:,0,1] = all_found_textline_polygons[i][:,0,1] - ydiff - return all_found_textline_polygons - - def run(self): """ Get image and scales, then extract the page of scanned image @@ -4432,7 +4368,7 @@ class Eynollah: all_found_textline_polygons=[ all_found_textline_polygons ] - all_found_textline_polygons = self.scale_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) order_text_new = [0] From 2d18739d9b267a14dfe0934b02772940976a8e72 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Sep 2024 15:08:09 +0200 Subject: [PATCH 241/412] postprocessing of textline contour dilation + skip layout and reading order passed as an argument --- qurator/eynollah/cli.py | 9 +++++++- qurator/eynollah/eynollah.py | 41 ++++++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 357582c..b293403 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -201,6 +201,12 @@ def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, i "-ncl", help="upper limit of columns in document image", ) +@click.option( + "--skip_layout_and_reading_order", + "-slro/-noslro", + is_flag=True, + help="if this parameter set to true, this tool will ignore layout detection and reading order. It means that textline detection will be done within printspace and contours of textline will be written in xml output file.", +) @click.option( "--log_level", "-l", @@ -208,7 +214,7 @@ def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, i help="Override log level globally to this", ) -def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, ignore_page_extraction, log_level): +def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, skip_layout_and_reading_order, ignore_page_extraction, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() @@ -247,6 +253,7 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s do_ocr=do_ocr, num_col_upper=num_col_upper, num_col_lower=num_col_lower, + skip_layout_and_reading_order=skip_layout_and_reading_order, ) if dir_in: eynollah.run() diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index cb70107..0619ef0 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -180,6 +180,7 @@ class Eynollah: do_ocr=False, num_col_upper=None, num_col_lower=None, + skip_layout_and_reading_order = False, override_dpi=None, logger=None, pcgts=None, @@ -213,6 +214,7 @@ class Eynollah: self.allow_scaling = allow_scaling self.headers_off = headers_off self.ignore_page_extraction = ignore_page_extraction + self.skip_layout_and_reading_order = skip_layout_and_reading_order self.ocr = do_ocr if num_col_upper: self.num_col_upper = int(num_col_upper) @@ -1951,7 +1953,7 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) - def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier, skip_layout_ro=False): + def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=False): self.logger.debug("enter get_regions_light_v") t_in = time.time() erosion_hurts = False @@ -2019,7 +2021,7 @@ class Eynollah: textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) - if not skip_layout_ro: + if not skip_layout_and_reading_order: #print("inside 2 ", time.time()-t_in) #print(img_resized.shape, num_col_classifier, "num_col_classifier") @@ -3818,6 +3820,30 @@ class Eynollah: def return_list_of_contours_with_desired_order(self, ls_cons, sorted_indexes): return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] + def return_it_in_two_groups(self,x_differential): + split = [ind if x_differential[ind]!=x_differential[ind+1] else -1 for ind in range(len(x_differential)-1)] + + split_masked = list( np.array(split[:])[np.array(split[:])!=-1] ) + + if 0 not in split_masked: + split_masked.insert(0, -1) + + split_masked.append(len(x_differential)-1) + + split_masked = np.array(split_masked) +1 + + sums = [np.sum(x_differential[split_masked[ind]:split_masked[ind+1]]) for ind in range(len(split_masked)-1)] + + indexes_to_bec_changed = [ind if ( np.abs(sums[ind-1]) > np.abs(sums[ind]) and np.abs(sums[ind+1]) > np.abs(sums[ind])) else -1 for ind in range(1,len(sums)-1) ] + + indexes_to_bec_changed_filtered = np.array(indexes_to_bec_changed)[np.array(indexes_to_bec_changed)!=-1] + + x_differential_new = np.copy(x_differential) + for i in indexes_to_bec_changed_filtered: + x_differential_new[split_masked[i]:split_masked[i+1]] = -1*np.array(x_differential)[split_masked[i]:split_masked[i+1]] + + return x_differential_new + def dilate_textlines(self,all_found_textline_polygons): for i in range(len(all_found_textline_polygons[0])): con_ind = all_found_textline_polygons[0][i] @@ -3863,6 +3889,8 @@ class Eynollah: y_differential = [-1 if y_differential[ind]<0 else 1 for ind in range(len(y_differential))] + y_differential = self.return_it_in_two_groups(y_differential) + y_differential = np.array(y_differential) @@ -3890,7 +3918,6 @@ class Eynollah: else: - y_biger_than_x = np.abs(y_differential) > np.abs(x_differential) mult = y_biger_than_x*y_differential @@ -3918,8 +3945,10 @@ class Eynollah: x_differential = [-1 if x_differential[ind]<0 else 1 for ind in range(len(x_differential))] + x_differential = self.return_it_in_two_groups(x_differential) x_differential = np.array(x_differential) + con_scaled = con_ind*1 con_scaled[:,0, 1] = con_ind[:,0,1] + 8*x_differential @@ -3949,8 +3978,6 @@ class Eynollah: Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - - skip_layout_ro = True t0_tot = time.time() @@ -3968,7 +3995,7 @@ class Eynollah: self.logger.info("Enhancing took %.1fs ", time.time() - t0) #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() - if not skip_layout_ro: + if not self.skip_layout_and_reading_order: if self.light_version: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) #print("text region early -2 in %.1fs", time.time() - t0) @@ -4356,7 +4383,7 @@ class Eynollah: return pcgts #print("text region early 7 in %.1fs", time.time() - t0) else: - _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_ro=skip_layout_ro) + _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) From b9e8959c4aefb0b9d24efb99abc309d7d350163c Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Sep 2024 16:33:13 +0200 Subject: [PATCH 242/412] update of light versions --- qurator/eynollah/eynollah.py | 244 ++++++++++++++++++----------------- 1 file changed, 129 insertions(+), 115 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 0619ef0..c7407e2 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1862,7 +1862,10 @@ class Eynollah: def textline_contours(self, img, patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') - thresholding_for_artificial_class_in_light_version = True#False + if self.textline_light: + thresholding_for_artificial_class_in_light_version = True#False + else: + thresholding_for_artificial_class_in_light_version = False if not self.dir_in: model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) #img = img.astype(np.uint8) @@ -2016,7 +2019,7 @@ class Eynollah: #print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) - textline_mask_tot_ea = self.run_textline(img_bin, num_col_classifier) + textline_mask_tot_ea = self.run_textline(img_resized, num_col_classifier) textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) @@ -2057,7 +2060,8 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=2) + #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + #mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_images_only=(prediction_regions_org[:,:] ==2)*1 @@ -2097,6 +2101,7 @@ class Eynollah: polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) + polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) text_regions_p_true = np.zeros(prediction_regions_org.shape) @@ -3845,132 +3850,139 @@ class Eynollah: return x_differential_new def dilate_textlines(self,all_found_textline_polygons): - for i in range(len(all_found_textline_polygons[0])): - con_ind = all_found_textline_polygons[0][i] - - con_ind = con_ind.astype(np.float) - - x_differential = np.diff( con_ind[:,0,0]) - y_differential = np.diff( con_ind[:,0,1]) - - x_min = float(np.min( con_ind[:,0,0] )) - y_min = float(np.min( con_ind[:,0,1] )) - - x_max = float(np.max( con_ind[:,0,0] )) - y_max = float(np.max( con_ind[:,0,1] )) + for j in range(len(all_found_textline_polygons)): + for i in range(len(all_found_textline_polygons[j])): + con_ind = all_found_textline_polygons[j][i] + + con_ind = con_ind.astype(np.float) + + x_differential = np.diff( con_ind[:,0,0]) + y_differential = np.diff( con_ind[:,0,1]) + + x_min = float(np.min( con_ind[:,0,0] )) + y_min = float(np.min( con_ind[:,0,1] )) + + x_max = float(np.max( con_ind[:,0,0] )) + y_max = float(np.max( con_ind[:,0,1] )) - - if (y_max - y_min) > (x_max - x_min) and (x_max - x_min)<70: - x_biger_than_x = np.abs(x_differential) > np.abs(y_differential) - - mult = x_biger_than_x*x_differential - - arg_min_mult = np.argmin(mult) - arg_max_mult = np.argmax(mult) - - if y_differential[0]==0: - y_differential[0] = 0.1 - - if y_differential[-1]==0: - y_differential[-1]= 0.1 + if (y_max - y_min) > (x_max - x_min) and (x_max - x_min)<70: + + x_biger_than_x = np.abs(x_differential) > np.abs(y_differential) + + mult = x_biger_than_x*x_differential + + arg_min_mult = np.argmin(mult) + arg_max_mult = np.argmax(mult) + + if y_differential[0]==0: + y_differential[0] = 0.1 + + if y_differential[-1]==0: + y_differential[-1]= 0.1 + + + + y_differential = [y_differential[ind] if y_differential[ind]!=0 else (y_differential[ind-1] + y_differential[ind+1])/2. for ind in range(len(y_differential)) ] + if y_differential[0]==0.1: + y_differential[0] = y_differential[1] + if y_differential[-1]==0.1: + y_differential[-1] = y_differential[-2] + + y_differential.append(y_differential[0]) - y_differential = [y_differential[ind] if y_differential[ind]!=0 else (y_differential[ind-1] + y_differential[ind+1])/2. for ind in range(len(y_differential)) ] - - - if y_differential[0]==0.1: - y_differential[0] = y_differential[1] - if y_differential[-1]==0.1: - y_differential[-1] = y_differential[-2] + y_differential = [-1 if y_differential[ind]<0 else 1 for ind in range(len(y_differential))] - y_differential.append(y_differential[0]) - - y_differential = [-1 if y_differential[ind]<0 else 1 for ind in range(len(y_differential))] - - y_differential = self.return_it_in_two_groups(y_differential) - - y_differential = np.array(y_differential) - - - con_scaled = con_ind*1 - - con_scaled[:,0, 0] = con_ind[:,0,0] - 8*y_differential - - con_scaled[arg_min_mult,0, 1] = con_ind[arg_min_mult,0,1] + 8 - con_scaled[arg_min_mult+1,0, 1] = con_ind[arg_min_mult+1,0,1] + 8 - - try: - con_scaled[arg_min_mult-1,0, 1] = con_ind[arg_min_mult-1,0,1] + 5 - con_scaled[arg_min_mult+2,0, 1] = con_ind[arg_min_mult+2,0,1] + 5 - except: - pass - - con_scaled[arg_max_mult,0, 1] = con_ind[arg_max_mult,0,1] - 8 - con_scaled[arg_max_mult+1,0, 1] = con_ind[arg_max_mult+1,0,1] - 8 - - try: - con_scaled[arg_max_mult-1,0, 1] = con_ind[arg_max_mult-1,0,1] - 5 - con_scaled[arg_max_mult+2,0, 1] = con_ind[arg_max_mult+2,0,1] - 5 - except: - pass - - - else: - y_biger_than_x = np.abs(y_differential) > np.abs(x_differential) - - mult = y_biger_than_x*y_differential - - arg_min_mult = np.argmin(mult) - arg_max_mult = np.argmax(mult) - - if x_differential[0]==0: - x_differential[0] = 0.1 - - if x_differential[-1]==0: - x_differential[-1]= 0.1 + y_differential = self.return_it_in_two_groups(y_differential) + + y_differential = np.array(y_differential) + con_scaled = con_ind*1 - x_differential = [x_differential[ind] if x_differential[ind]!=0 else (x_differential[ind-1] + x_differential[ind+1])/2. for ind in range(len(x_differential)) ] - - - if x_differential[0]==0.1: - x_differential[0] = x_differential[1] - if x_differential[-1]==0.1: - x_differential[-1] = x_differential[-2] + con_scaled[:,0, 0] = con_ind[:,0,0] - 8*y_differential - x_differential.append(x_differential[0]) - - x_differential = [-1 if x_differential[ind]<0 else 1 for ind in range(len(x_differential))] - - x_differential = self.return_it_in_two_groups(x_differential) - x_differential = np.array(x_differential) + con_scaled[arg_min_mult,0, 1] = con_ind[arg_min_mult,0,1] + 8 + con_scaled[arg_min_mult+1,0, 1] = con_ind[arg_min_mult+1,0,1] + 8 + + try: + con_scaled[arg_min_mult-1,0, 1] = con_ind[arg_min_mult-1,0,1] + 5 + con_scaled[arg_min_mult+2,0, 1] = con_ind[arg_min_mult+2,0,1] + 5 + except: + pass + + con_scaled[arg_max_mult,0, 1] = con_ind[arg_max_mult,0,1] - 8 + con_scaled[arg_max_mult+1,0, 1] = con_ind[arg_max_mult+1,0,1] - 8 + + try: + con_scaled[arg_max_mult-1,0, 1] = con_ind[arg_max_mult-1,0,1] - 5 + con_scaled[arg_max_mult+2,0, 1] = con_ind[arg_max_mult+2,0,1] - 5 + except: + pass - con_scaled = con_ind*1 + else: + y_biger_than_x = np.abs(y_differential) > np.abs(x_differential) + + mult = y_biger_than_x*y_differential + + arg_min_mult = np.argmin(mult) + arg_max_mult = np.argmax(mult) + + if x_differential[0]==0: + x_differential[0] = 0.1 + + if x_differential[-1]==0: + x_differential[-1]= 0.1 + + + + x_differential = [x_differential[ind] if x_differential[ind]!=0 else (x_differential[ind-1] + x_differential[ind+1])/2. for ind in range(len(x_differential)) ] + + + if x_differential[0]==0.1: + x_differential[0] = x_differential[1] + if x_differential[-1]==0.1: + x_differential[-1] = x_differential[-2] + + x_differential.append(x_differential[0]) + + x_differential = [-1 if x_differential[ind]<0 else 1 for ind in range(len(x_differential))] + + x_differential = self.return_it_in_two_groups(x_differential) + x_differential = np.array(x_differential) + + + con_scaled = con_ind*1 + + con_scaled[:,0, 1] = con_ind[:,0,1] + 8*x_differential + + con_scaled[arg_min_mult,0, 0] = con_ind[arg_min_mult,0,0] + 8 + con_scaled[arg_min_mult+1,0, 0] = con_ind[arg_min_mult+1,0,0] + 8 + + try: + con_scaled[arg_min_mult-1,0, 0] = con_ind[arg_min_mult-1,0,0] + 5 + con_scaled[arg_min_mult+2,0, 0] = con_ind[arg_min_mult+2,0,0] + 5 + except: + pass + + con_scaled[arg_max_mult,0, 0] = con_ind[arg_max_mult,0,0] - 8 + con_scaled[arg_max_mult+1,0, 0] = con_ind[arg_max_mult+1,0,0] - 8 + + try: + con_scaled[arg_max_mult-1,0, 0] = con_ind[arg_max_mult-1,0,0] - 5 + con_scaled[arg_max_mult+2,0, 0] = con_ind[arg_max_mult+2,0,0] - 5 + except: + pass + - con_scaled[:,0, 1] = con_ind[:,0,1] + 8*x_differential + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 + con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 - con_scaled[arg_min_mult,0, 0] = con_ind[arg_min_mult,0,0] + 8 - con_scaled[arg_min_mult+1,0, 0] = con_ind[arg_min_mult+1,0,0] + 8 - - con_scaled[arg_min_mult-1,0, 0] = con_ind[arg_min_mult-1,0,0] + 5 - con_scaled[arg_min_mult+2,0, 0] = con_ind[arg_min_mult+2,0,0] + 5 - - con_scaled[arg_max_mult,0, 0] = con_ind[arg_max_mult,0,0] - 8 - con_scaled[arg_max_mult+1,0, 0] = con_ind[arg_max_mult+1,0,0] - 8 - - con_scaled[arg_max_mult-1,0, 0] = con_ind[arg_max_mult-1,0,0] - 5 - con_scaled[arg_max_mult+2,0, 0] = con_ind[arg_max_mult+2,0,0] - 5 - - - con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 - con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 - - all_found_textline_polygons[0][i][:,0,1] = con_scaled[:,0, 1] - all_found_textline_polygons[0][i][:,0,0] = con_scaled[:,0, 0] + all_found_textline_polygons[j][i][:,0,1] = con_scaled[:,0, 1] + all_found_textline_polygons[j][i][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons def run(self): @@ -4207,6 +4219,8 @@ class Eynollah: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + else: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) From 5d680136a4ed752e398cd47d3be0fd5aaf698f13 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 21 Sep 2024 01:04:28 +0200 Subject: [PATCH 243/412] updating light version --- qurator/eynollah/eynollah.py | 45 ++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index c7407e2..629818f 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -260,7 +260,7 @@ class Eynollah: if self.textline_light: self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: - self.model_textline_dir = dir_models + "/eynollah-textline_20210425" + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" if self.ocr: self.model_ocr_dir = dir_models + "/checkpoint-166692_printed_trocr" @@ -1916,11 +1916,7 @@ class Eynollah: prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) - - if self.textline_light: - return (prediction_textline[:, :, 0]==1)*1, (prediction_textline_longshot_true_size[:, :, 0]==1)*1 - else: - return prediction_textline[:, :, 0], prediction_textline_longshot_true_size[:, :, 0] + return ((prediction_textline[:, :, 0]==1)*1).astype('uint8'), ((prediction_textline_longshot_true_size[:, :, 0]==1)*1).astype('uint8') def do_work_of_slopes(self, q, poly, box_sub, boxes_per_process, textline_mask_tot, contours_per_process): @@ -1996,7 +1992,7 @@ class Eynollah: #if (not self.input_binary) or self.full_layout: #if self.input_binary: #img_bin = np.copy(img_resized) - if (not self.input_binary and self.full_layout):# or (not self.input_binary and num_col_classifier >= 3): + if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 3): if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) @@ -4066,8 +4062,35 @@ class Eynollah: t1 = time.time() #plt.imshow(table_prediction) #plt.show() - + if self.light_version and num_col_classifier in (1,2): + org_h_l_m = textline_mask_tot_ea.shape[0] + org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1: + img_w_new = 2000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 2400 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + image_page = resize_image(image_page,img_h_new, img_w_new ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + mask_images = resize_image(mask_images,img_h_new, img_w_new ) + mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) + text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) + table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + + if self.light_version and num_col_classifier in (1,2): + image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) + text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) + textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) + text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) + table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) + image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) + self.logger.info("detection of marginals took %.1fs", time.time() - t1) #print("text region early 2 marginal in %.1fs", time.time() - t0) t1 = time.time() @@ -4222,18 +4245,20 @@ class Eynollah: all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) else: + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: From 7f08458436d1f6aad43f809b3a388c8c275d44f7 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 21 Sep 2024 14:39:54 +0200 Subject: [PATCH 244/412] dilation of text regions without opencv --- qurator/eynollah/eynollah.py | 84 +++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 629818f..b2dea47 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -37,9 +37,7 @@ from tensorflow.keras.models import load_model sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") -from scipy.signal import find_peaks import matplotlib.pyplot as plt -from scipy.ndimage import gaussian_filter1d from tensorflow.python.keras.backend import set_session from tensorflow.keras import layers @@ -2056,8 +2054,8 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) - #mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) + mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_images_only=(prediction_regions_org[:,:] ==2)*1 @@ -2097,6 +2095,8 @@ class Eynollah: polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) + ##polygons_of_only_texts = self.dilate_textregions_contours(polygons_of_only_texts) + polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) @@ -3845,6 +3845,79 @@ class Eynollah: return x_differential_new + def dilate_textregions_contours(self,all_found_textline_polygons): + for j in range(len(all_found_textline_polygons)): + + con_ind = all_found_textline_polygons[j] + + con_ind = con_ind.astype(np.float) + + x_differential = np.diff( con_ind[:,0,0]) + y_differential = np.diff( con_ind[:,0,1]) + + x_differential = gaussian_filter1d(x_differential, 3) + y_differential = gaussian_filter1d(y_differential, 3) + + x_min = float(np.min( con_ind[:,0,0] )) + y_min = float(np.min( con_ind[:,0,1] )) + + x_max = float(np.max( con_ind[:,0,0] )) + y_max = float(np.max( con_ind[:,0,1] )) + + x_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in x_differential] + y_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in y_differential] + + abs_diff=abs(abs(x_differential)- abs(y_differential) ) + + inc_x = np.zeros(len(x_differential)+1) + inc_y = np.zeros(len(x_differential)+1) + + for i in range(len(x_differential)): + if abs_diff[i]==0: + inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: + inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: + inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + + elif abs_diff[i]!=0 and abs_diff[i]>=3: + if abs(x_differential[i])>abs(y_differential[i]): + inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + else: + inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + else: + inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) + + ###inc_x =list(inc_x) + ###inc_x.append(inc_x[0]) + + ###inc_y =list(inc_y) + ###inc_y.append(inc_y[0]) + + inc_x[0] = inc_x[-1] + inc_y[0] = inc_y[-1] + + con_scaled = con_ind*1 + + con_scaled[:,0, 0] = con_ind[:,0,0] + np.array(inc_x)[:] + con_scaled[:,0, 1] = con_ind[:,0,1] + np.array(inc_y)[:] + + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 + con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 + + all_found_textline_polygons[j][:,0,1] = con_scaled[:,0, 1] + all_found_textline_polygons[j][:,0,0] = con_scaled[:,0, 0] + return all_found_textline_polygons + + + + + + + + def dilate_textlines(self,all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): for i in range(len(all_found_textline_polygons[j])): @@ -4096,7 +4169,7 @@ class Eynollah: t1 = time.time() if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - + polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.full_layout: if not self.light_version: img_bin_light = None @@ -4230,6 +4303,7 @@ class Eynollah: #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) + txt_con_org = self.dilate_textregions_contours(txt_con_org) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) #print("text region early 4 in %.1fs", time.time() - t0) From 62f8ae486043ddf9e39b057e754cc28081275ce3 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 23 Sep 2024 14:03:07 +0200 Subject: [PATCH 245/412] updating dilation of textlines and text regions --- qurator/eynollah/eynollah.py | 96 +++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index b2dea47..fb2d699 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3855,6 +3855,7 @@ class Eynollah: x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) + x_differential = gaussian_filter1d(x_differential, 3) y_differential = gaussian_filter1d(y_differential, 3) @@ -3912,6 +3913,93 @@ class Eynollah: return all_found_textline_polygons + def dilate_textline_contours(self,all_found_textline_polygons): + for j in range(len(all_found_textline_polygons)): + for ij in range(len(all_found_textline_polygons[j])): + + con_ind = all_found_textline_polygons[j][ij] + + con_ind = con_ind.astype(np.float) + + x_differential = np.diff( con_ind[:,0,0]) + y_differential = np.diff( con_ind[:,0,1]) + + x_differential = gaussian_filter1d(x_differential, 3) + y_differential = gaussian_filter1d(y_differential, 3) + + x_min = float(np.min( con_ind[:,0,0] )) + y_min = float(np.min( con_ind[:,0,1] )) + + x_max = float(np.max( con_ind[:,0,0] )) + y_max = float(np.max( con_ind[:,0,1] )) + + x_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in x_differential] + y_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in y_differential] + + abs_diff=abs(abs(x_differential)- abs(y_differential) ) + + inc_x = np.zeros(len(x_differential)+1) + inc_y = np.zeros(len(x_differential)+1) + + + #print(y_max-y_min, x_max-x_min,(y_max-y_min)/(x_max-x_min), (x_max-x_min)/(y_max-y_min) ) + ##if (y_max-y_min)<40: + ##dilation_m1 = 5 + ##dilation_m2 = int(dilation_m1/2.) +1 + ##else: + ##dilation_m1 = 12 + ##dilation_m2 = int(dilation_m1/2.) +1 + + if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: + dilation_m1 = int( (y_max-y_min) * 5/20.0 ) + elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.3 and (x_max-x_min)>50: + dilation_m1 = int( (y_max-y_min) * 1/20.0 ) + elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: + dilation_m1 = int( (x_max-x_min) * 5/20.0 ) + elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.3 and (y_max-y_min)>50: + dilation_m1 = int( (x_max-x_min) * 1/20.0 ) + else: + dilation_m1 = int( (y_max-y_min) * 4/20.0 ) + dilation_m2 = int(dilation_m1/2.) +1 + + for i in range(len(x_differential)): + if abs_diff[i]==0: + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) + + elif abs_diff[i]!=0 and abs_diff[i]>=3: + if abs(x_differential[i])>abs(y_differential[i]): + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) + else: + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) + else: + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) + + ###inc_x =list(inc_x) + ###inc_x.append(inc_x[0]) + + ###inc_y =list(inc_y) + ###inc_y.append(inc_y[0]) + + inc_x[0] = inc_x[-1] + inc_y[0] = inc_y[-1] + + con_scaled = con_ind*1 + + con_scaled[:,0, 0] = con_ind[:,0,0] + np.array(inc_x)[:] + con_scaled[:,0, 1] = con_ind[:,0,1] + np.array(inc_y)[:] + + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 + con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 + + all_found_textline_polygons[j][ij][:,0,1] = con_scaled[:,0, 1] + all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] + return all_found_textline_polygons @@ -4174,6 +4262,7 @@ class Eynollah: if not self.light_version: img_bin_light = None polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) + polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 @@ -4304,6 +4393,7 @@ class Eynollah: if self.light_version: txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) txt_con_org = self.dilate_textregions_contours(txt_con_org) + contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) #print("text region early 4 in %.1fs", time.time() - t0) @@ -4316,7 +4406,9 @@ class Eynollah: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons_marginals = self.dilate_textline_contours(all_found_textline_polygons_marginals) else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) @@ -4508,7 +4600,7 @@ class Eynollah: all_found_textline_polygons=[ all_found_textline_polygons ] - all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) order_text_new = [0] From 6626dc68660d239cf8a4a15b64e8bb670e395409 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 23 Sep 2024 15:50:37 +0200 Subject: [PATCH 246/412] updating textline dilation parameters --- qurator/eynollah/eynollah.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index fb2d699..a69854d 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3950,16 +3950,26 @@ class Eynollah: ##dilation_m1 = 12 ##dilation_m2 = int(dilation_m1/2.) +1 - if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: + if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.15 and (x_max-x_min)>50: dilation_m1 = int( (y_max-y_min) * 5/20.0 ) + elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.15 and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: + dilation_m1 = int( (y_max-y_min) * 2/20.0 ) elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.3 and (x_max-x_min)>50: dilation_m1 = int( (y_max-y_min) * 1/20.0 ) - elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: + elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.15 and (y_max-y_min)>50: dilation_m1 = int( (x_max-x_min) * 5/20.0 ) + elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.15 and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: + dilation_m1 = int( (x_max-x_min) * 2/20.0 ) elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.3 and (y_max-y_min)>50: dilation_m1 = int( (x_max-x_min) * 1/20.0 ) else: dilation_m1 = int( (y_max-y_min) * 4/20.0 ) + + if dilation_m1>12: + dilation_m1 = 12 + if dilation_m1<4: + dilation_m1 = 4 + #print(dilation_m1, 'dilation_m1') dilation_m2 = int(dilation_m1/2.) +1 for i in range(len(x_differential)): From b33739adeef5cd40b48faa3a955cd1d473b5e250 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 24 Sep 2024 16:06:27 +0200 Subject: [PATCH 247/412] parametriyation in the case of textline contours dilation is accomplished --- qurator/eynollah/eynollah.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index a69854d..8c0979d 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -3919,6 +3919,8 @@ class Eynollah: con_ind = all_found_textline_polygons[j][ij] + area = cv2.contourArea(con_ind) + con_ind = con_ind.astype(np.float) x_differential = np.diff( con_ind[:,0,0]) @@ -3943,6 +3945,7 @@ class Eynollah: #print(y_max-y_min, x_max-x_min,(y_max-y_min)/(x_max-x_min), (x_max-x_min)/(y_max-y_min) ) + #print(area / (x_max-x_min)) ##if (y_max-y_min)<40: ##dilation_m1 = 5 ##dilation_m2 = int(dilation_m1/2.) +1 @@ -3950,20 +3953,26 @@ class Eynollah: ##dilation_m1 = 12 ##dilation_m2 = int(dilation_m1/2.) +1 - if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.15 and (x_max-x_min)>50: - dilation_m1 = int( (y_max-y_min) * 5/20.0 ) - elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.15 and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: - dilation_m1 = int( (y_max-y_min) * 2/20.0 ) - elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.3 and (x_max-x_min)>50: - dilation_m1 = int( (y_max-y_min) * 1/20.0 ) - elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.15 and (y_max-y_min)>50: - dilation_m1 = int( (x_max-x_min) * 5/20.0 ) - elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.15 and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: - dilation_m1 = int( (x_max-x_min) * 2/20.0 ) - elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.3 and (y_max-y_min)>50: - dilation_m1 = int( (x_max-x_min) * 1/20.0 ) + #########if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.15 and (x_max-x_min)>50: + #########dilation_m1 = int( (y_max-y_min) * 5/20.0 ) + #########elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.15 and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: + #########dilation_m1 = int( (y_max-y_min) * 2/20.0 ) + #########elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.3 and (x_max-x_min)>50: + #########dilation_m1 = int( (y_max-y_min) * 1/20.0 ) + #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.15 and (y_max-y_min)>50: + #########dilation_m1 = int( (x_max-x_min) * 5/20.0 ) + #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.15 and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: + #########dilation_m1 = int( (x_max-x_min) * 2/20.0 ) + #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.3 and (y_max-y_min)>50: + #########dilation_m1 = int( (x_max-x_min) * 1/20.0 ) + #########else: + #########dilation_m1 = int( (y_max-y_min) * 4/20.0 ) + + if (y_max-y_min) <= (x_max-x_min): + dilation_m1 = round(area / (x_max-x_min) * 0.35) else: - dilation_m1 = int( (y_max-y_min) * 4/20.0 ) + dilation_m1 = round(area / (y_max-y_min) * 0.35) + if dilation_m1>12: dilation_m1 = 12 From 95effe54a0159811b80c7ca5bd9147d196ef5187 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 25 Sep 2024 20:00:53 +0200 Subject: [PATCH 248/412] updating textregions dilation --- qurator/eynollah/eynollah.py | 151 ++++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 12 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 8c0979d..794ebe6 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2054,7 +2054,7 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_images_only=(prediction_regions_org[:,:] ==2)*1 @@ -3846,18 +3846,22 @@ class Eynollah: return x_differential_new def dilate_textregions_contours(self,all_found_textline_polygons): + #print(all_found_textline_polygons) for j in range(len(all_found_textline_polygons)): con_ind = all_found_textline_polygons[j] - + area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) + con_ind[:,0,0] = gaussian_filter1d(con_ind[:,0,0], 0.1) + con_ind[:,0,1] = gaussian_filter1d(con_ind[:,0,1], 0.1) + x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) - x_differential = gaussian_filter1d(x_differential, 3) - y_differential = gaussian_filter1d(y_differential, 3) + x_differential = gaussian_filter1d(x_differential, .5) + y_differential = gaussian_filter1d(y_differential, .5) x_min = float(np.min( con_ind[:,0,0] )) y_min = float(np.min( con_ind[:,0,1] )) @@ -3873,23 +3877,54 @@ class Eynollah: inc_x = np.zeros(len(x_differential)+1) inc_y = np.zeros(len(x_differential)+1) + + if (y_max-y_min) <= (x_max-x_min): + dilation_m1 = round(area / (x_max-x_min) * 0.12) + else: + dilation_m1 = round(area / (y_max-y_min) * 0.12) + + if dilation_m1>8: + dilation_m1 = 8 + if dilation_m1<5: + dilation_m1 = 5 + #print(dilation_m1, 'dilation_m1') + dilation_m2 = int(dilation_m1/2.) +1 + for i in range(len(x_differential)): if abs_diff[i]==0: - inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) - inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: - inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: - inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) elif abs_diff[i]!=0 and abs_diff[i]>=3: if abs(x_differential[i])>abs(y_differential[i]): - inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) else: - inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) else: - inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) - inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) + + ###for i in range(len(x_differential)): + ###if abs_diff[i]==0: + ###inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) + ###inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) + ###elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: + ###inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + ###elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: + ###inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + + ###elif abs_diff[i]!=0 and abs_diff[i]>=3: + ###if abs(x_differential[i])>abs(y_differential[i]): + ###inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) + ###else: + ###inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) + ###else: + ###inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) + ###inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) ###inc_x =list(inc_x) ###inc_x.append(inc_x[0]) @@ -3908,6 +3943,98 @@ class Eynollah: con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 + area_scaled = cv2.contourArea(con_scaled.astype(np.int32)) + + con_ind = con_ind.astype(np.int32) + + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] + + results = np.array(results) + + #print(results,'results') + + results[results==0] = 1 + + + diff_result = np.diff(results) + + indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] + indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] + + #print(area_scaled / area, "ratio") + #print(results,'results') + #if results[0]==1 and diff_result[-1]==-2: + ##indices_2 = indices_2[1:] + ##indices_m2 = indices_m2[1:] + + #con_scaled[:indices_m2[0]+1,0, 1] = con_scaled[indices_m2[-1],0, 1] + #con_scaled[:indices_m2[0]+1,0, 0] = con_scaled[indices_m2[-1],0, 0] + + + #con_scaled[indices_2[-1]+1:,0, 1] = con_scaled[indices_m2[-1],0, 1] + #con_scaled[indices_2[-1]+1:,0, 0] = con_scaled[indices_m2[-1],0, 0] + + #indices_2 = indices_2[:-1] + #indices_m2 = indices_m2[1:-1] + + if results[0]==1: + con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] + con_scaled[:indices_m2[0]+1,0, 0] = con_ind[:indices_m2[0]+1,0,0] + #indices_2 = indices_2[1:] + indices_m2 = indices_m2[1:] + + + + if len(indices_2)>len(indices_m2): + con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] + con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] + + indices_2 = indices_2[:-1] + + + + #diff_neg_pos = np.array(indices_m2) - np.array(indices_2) + + + #print(diff_neg_pos,'diff') + ##print(indices_2, 'indices_2') + #indices_2 = np.array(indices_2)[diff_neg_pos>1] + #indices_m2 = np.array(indices_m2)[diff_neg_pos>1] + + for ii in range(len(indices_2)): + + #x_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 0] + #y_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 1] + + #if x_inner[-1]>=x_inner[0]: + #x_interest = np.min(x_inner) + #else: + #x_interest = np.max(x_inner) + + #if y_inner[-1]>=y_inner[0]: + #y_interest = np.min(y_inner) + #else: + #y_interest = np.max(y_inner) + + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] + + + + #con_scaled[:,0, 1][results[:]>0] = con_ind[:,0,1][results[:]>0] + #con_scaled[:,0, 0][results[:]>0] = con_ind[:,0,0][results[:]>0] + + #print(list(results), 'results') + #print(list(diff_result), 'diff_result') + #print(indices_2,'2') + #print(indices_m2,'-2') + #print(diff_neg_pos,'diff_neg_pos') + + #con_scaled[:,0, 1] = gaussian_filter1d(con_scaled[:,0, 1], 0.1) + #con_scaled[:,0, 0] = gaussian_filter1d(con_scaled[:,0, 0], 0.1) + + con_scaled[-1,0, 1] = con_scaled[0,0, 1] + con_scaled[-1,0, 0] = con_scaled[0,0, 0] all_found_textline_polygons[j][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons From 133091137dc01f04eedf153119a04559a8f0633d Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 27 Sep 2024 13:57:01 +0200 Subject: [PATCH 249/412] dilation of textregions and marginals are accomplished --- qurator/eynollah/eynollah.py | 452 ++++++++++++++++++++++++----------- 1 file changed, 313 insertions(+), 139 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 794ebe6..2fe7325 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -1050,7 +1050,7 @@ class Eynollah: #del model #gc.collect() return prediction_true - def do_prediction_new_concept(self, patches, img, model, marginal_of_patch_percent=0.1): + def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction") img_height_model = model.layers[len(model.layers) - 1].output_shape[1] @@ -1064,14 +1064,14 @@ class Eynollah: label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), verbose=0) - seg_not_base = label_p_pred[0,:,:,4] + #seg_not_base = label_p_pred[0,:,:,4] - seg_not_base[seg_not_base>0.4] =1 - seg_not_base[seg_not_base<1] =0 + #seg_not_base[seg_not_base>0.4] =1 + #seg_not_base[seg_not_base<1] =0 seg = np.argmax(label_p_pred, axis=3)[0] - seg[seg_not_base==1]=4 + #seg[seg_not_base==1]=4 seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) @@ -1099,6 +1099,16 @@ class Eynollah: nyf = img_h / float(height_mid) nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) for i in range(nxf): for j in range(nyf): @@ -1120,44 +1130,57 @@ class Eynollah: if index_y_u > img_h: index_y_u = img_h index_y_d = img_h - img_height_model + + + list_i_s.append(i) + list_j_s.append(j) + list_x_u.append(index_x_u) + list_x_d.append(index_x_d) + list_y_d.append(index_y_d) + list_y_u.append(index_y_u) + - img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), - verbose=0) - seg = np.argmax(label_p_pred, axis=3)[0] + img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] + + batch_indexer = batch_indexer + 1 + + #img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] + #label_p_pred = model.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), + #verbose=0) + #seg = np.argmax(label_p_pred, axis=3)[0] - seg_not_base = label_p_pred[0,:,:,4] - ##seg2 = -label_p_pred[0,:,:,2] + ######seg_not_base = label_p_pred[0,:,:,4] + ########seg2 = -label_p_pred[0,:,:,2] - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 + ######seg_not_base[seg_not_base>0.03] =1 + ######seg_not_base[seg_not_base<1] =0 - seg_test = label_p_pred[0,:,:,1] - ##seg2 = -label_p_pred[0,:,:,2] + ######seg_test = label_p_pred[0,:,:,1] + ########seg2 = -label_p_pred[0,:,:,2] - seg_test[seg_test>0.75] =1 - seg_test[seg_test<1] =0 + ######seg_test[seg_test>0.75] =1 + ######seg_test[seg_test<1] =0 - seg_line = label_p_pred[0,:,:,3] - ##seg2 = -label_p_pred[0,:,:,2] + ######seg_line = label_p_pred[0,:,:,3] + ########seg2 = -label_p_pred[0,:,:,2] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 + ######seg_line[seg_line>0.1] =1 + ######seg_line[seg_line<1] =0 - seg_background = label_p_pred[0,:,:,0] - ##seg2 = -label_p_pred[0,:,:,2] + ######seg_background = label_p_pred[0,:,:,0] + ########seg2 = -label_p_pred[0,:,:,2] - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 + ######seg_background[seg_background>0.25] =1 + ######seg_background[seg_background<1] =0 ##seg = seg+seg2 #seg = label_p_pred[0,:,:,2] #seg[seg>0.4] =1 @@ -1170,56 +1193,221 @@ class Eynollah: ##plt.show() #seg[seg==1]=0 #seg[seg_test==1]=1 - seg[seg_not_base==1]=4 - seg[seg_background==1]=0 - seg[(seg_line==1) & (seg==0)]=3 - seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + ######seg[seg_not_base==1]=4 + ######seg[seg_background==1]=0 + ######seg[(seg_line==1) & (seg==0)]=3 + #seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) - if i == 0 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i == 0 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i == 0 and j != 0 and j != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color - elif i == nxf - 1 and j != 0 and j != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color - elif i != 0 and i != nxf - 1 and j == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] - mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color - elif i != 0 and i != nxf - 1 and j == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] - mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color + #if i == 0 and j == 0: + #seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + #seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + #prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color + #elif i == nxf - 1 and j == nyf - 1: + #seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0] = seg + #prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg_color + #elif i == 0 and j == nyf - 1: + #seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + #seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin] = seg + #prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg_color + #elif i == nxf - 1 and j == 0: + #seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + #prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color + #elif i == 0 and j != 0 and j != nyf - 1: + #seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + #seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin] = seg + #prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg_color + #elif i == nxf - 1 and j != 0 and j != nyf - 1: + #seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0] = seg + #prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg_color + #elif i != 0 and i != nxf - 1 and j == 0: + #seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + #seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] + #mask_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + #prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color + #elif i != 0 and i != nxf - 1 and j == nyf - 1: + #seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + #seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin] = seg + #prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg_color + #else: + #seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + #seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] + #mask_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin] = seg + #prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg_color + + + if batch_indexer == n_batch_inference: + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + elif i==(nxf-1) and j==(nyf-1): + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) prediction_true = prediction_true.astype(np.uint8) return prediction_true @@ -1963,7 +2151,7 @@ class Eynollah: #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: - img_w_new = 800#1000 + img_w_new = 1000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: @@ -1971,17 +2159,17 @@ class Eynollah: img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 3: - img_w_new = 1600#2000 + img_w_new = 2000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 4: - img_w_new = 1900#2500 + img_w_new = 2500 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 5: - img_w_new = 2300#3000 + img_w_new = 3000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) else: - img_w_new = 3000#4000 + img_w_new = 4000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) img_resized = resize_image(img,img_h_new, img_w_new ) @@ -2025,17 +2213,17 @@ class Eynollah: if not self.dir_in: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region) + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region, n_batch_inference=1) else: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light, n_batch_inference=3) prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: if num_col_classifier == 1 or num_col_classifier == 2: - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2) + prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2, n_batch_inference=1) else: - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region) + prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) @@ -2054,8 +2242,12 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) - mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) + ##if num_col_classifier == 1 or num_col_classifier == 2: + ###mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + ##mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) + + mask_texts_only = cv2.dilate(mask_texts_only, kernel=np.ones((2,2), np.uint8), iterations=1) + mask_images_only=(prediction_regions_org[:,:] ==2)*1 @@ -3150,7 +3342,14 @@ class Eynollah: pixel_img = 4 min_area_mar = 0.00001 - polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + if self.light_version: + marginal_mask = (text_regions_p[:,:]==pixel_img)*1 + marginal_mask = marginal_mask.astype('uint8') + marginal_mask = cv2.dilate(marginal_mask, KERNEL, iterations=2) + + polygons_of_marginals = return_contours_of_interested_region(marginal_mask, 1, min_area_mar) + else: + polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) @@ -3241,7 +3440,15 @@ class Eynollah: pixel_img = 4 min_area_mar = 0.00001 - polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) + + if self.light_version: + marginal_mask = (text_regions_p[:,:]==pixel_img)*1 + marginal_mask = marginal_mask.astype('uint8') + marginal_mask = cv2.dilate(marginal_mask, KERNEL, iterations=2) + + polygons_of_marginals = return_contours_of_interested_region(marginal_mask, 1, min_area_mar) + else: + polygons_of_marginals = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) @@ -3850,18 +4057,19 @@ class Eynollah: for j in range(len(all_found_textline_polygons)): con_ind = all_found_textline_polygons[j] + #print(len(con_ind[:,0,0]),'con_ind[:,0,0]') area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) - con_ind[:,0,0] = gaussian_filter1d(con_ind[:,0,0], 0.1) - con_ind[:,0,1] = gaussian_filter1d(con_ind[:,0,1], 0.1) + #con_ind[:,0,0] = gaussian_filter1d(con_ind[:,0,0], 0.5) + #con_ind[:,0,1] = gaussian_filter1d(con_ind[:,0,1], 0.5) x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) - x_differential = gaussian_filter1d(x_differential, .5) - y_differential = gaussian_filter1d(y_differential, .5) + x_differential = gaussian_filter1d(x_differential, 0.1) + y_differential = gaussian_filter1d(y_differential, 0.1) x_min = float(np.min( con_ind[:,0,0] )) y_min = float(np.min( con_ind[:,0,1] )) @@ -3885,8 +4093,8 @@ class Eynollah: if dilation_m1>8: dilation_m1 = 8 - if dilation_m1<5: - dilation_m1 = 5 + if dilation_m1<6: + dilation_m1 = 6 #print(dilation_m1, 'dilation_m1') dilation_m2 = int(dilation_m1/2.) +1 @@ -4002,7 +4210,6 @@ class Eynollah: #indices_m2 = np.array(indices_m2)[diff_neg_pos>1] for ii in range(len(indices_2)): - #x_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 0] #y_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 1] @@ -4030,11 +4237,12 @@ class Eynollah: #print(indices_m2,'-2') #print(diff_neg_pos,'diff_neg_pos') - #con_scaled[:,0, 1] = gaussian_filter1d(con_scaled[:,0, 1], 0.1) - #con_scaled[:,0, 0] = gaussian_filter1d(con_scaled[:,0, 0], 0.1) + ##con_scaled[:,0, 1] = gaussian_filter1d(con_scaled[:,0, 1], 0.1) + ##con_scaled[:,0, 0] = gaussian_filter1d(con_scaled[:,0, 0], 0.1) - con_scaled[-1,0, 1] = con_scaled[0,0, 1] - con_scaled[-1,0, 0] = con_scaled[0,0, 0] + #con_scaled[-1,0, 1] = con_scaled[0,0, 1] + #con_scaled[-1,0, 0] = con_scaled[0,0, 0] + ##print(len(con_scaled[:,0,0]),'con_scaled[:,0,0]') all_found_textline_polygons[j][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons @@ -4045,7 +4253,7 @@ class Eynollah: for ij in range(len(all_found_textline_polygons[j])): con_ind = all_found_textline_polygons[j][ij] - + print(len(con_ind[:,0,0]),'con_ind[:,0,0]') area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) @@ -4069,31 +4277,6 @@ class Eynollah: inc_x = np.zeros(len(x_differential)+1) inc_y = np.zeros(len(x_differential)+1) - - - #print(y_max-y_min, x_max-x_min,(y_max-y_min)/(x_max-x_min), (x_max-x_min)/(y_max-y_min) ) - #print(area / (x_max-x_min)) - ##if (y_max-y_min)<40: - ##dilation_m1 = 5 - ##dilation_m2 = int(dilation_m1/2.) +1 - ##else: - ##dilation_m1 = 12 - ##dilation_m2 = int(dilation_m1/2.) +1 - - #########if (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))<0.15 and (x_max-x_min)>50: - #########dilation_m1 = int( (y_max-y_min) * 5/20.0 ) - #########elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.15 and ((y_max-y_min)/(x_max-x_min))<0.3 and (x_max-x_min)>50: - #########dilation_m1 = int( (y_max-y_min) * 2/20.0 ) - #########elif (y_max-y_min) <= (x_max-x_min) and ((y_max-y_min)/(x_max-x_min))>=0.3 and (x_max-x_min)>50: - #########dilation_m1 = int( (y_max-y_min) * 1/20.0 ) - #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))<0.15 and (y_max-y_min)>50: - #########dilation_m1 = int( (x_max-x_min) * 5/20.0 ) - #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.15 and ((x_max-x_min)/(y_max-y_min))<0.3 and (y_max-y_min)>50: - #########dilation_m1 = int( (x_max-x_min) * 2/20.0 ) - #########elif (x_max-x_min) < (y_max-y_min) and ((x_max-x_min)/(y_max-y_min))>=0.3 and (y_max-y_min)>50: - #########dilation_m1 = int( (x_max-x_min) * 1/20.0 ) - #########else: - #########dilation_m1 = int( (y_max-y_min) * 4/20.0 ) if (y_max-y_min) <= (x_max-x_min): dilation_m1 = round(area / (x_max-x_min) * 0.35) @@ -4126,11 +4309,6 @@ class Eynollah: inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) - ###inc_x =list(inc_x) - ###inc_x.append(inc_x[0]) - - ###inc_y =list(inc_y) - ###inc_y.append(inc_y[0]) inc_x[0] = inc_x[-1] inc_y[0] = inc_y[-1] @@ -4146,11 +4324,6 @@ class Eynollah: all_found_textline_polygons[j][ij][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons - - - - - def dilate_textlines(self,all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): @@ -4403,12 +4576,12 @@ class Eynollah: t1 = time.time() if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.full_layout: if not self.light_version: img_bin_light = None polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) - polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 @@ -4537,9 +4710,10 @@ class Eynollah: #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) - txt_con_org = self.dilate_textregions_contours(txt_con_org) contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) + #txt_con_org = self.dilate_textregions_contours(txt_con_org) + #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) #print("text region early 4 in %.1fs", time.time() - t0) From ad323162173f651e9c5f2cb28804c23a582432d5 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 27 Sep 2024 20:59:01 +0200 Subject: [PATCH 250/412] updating light version --- qurator/eynollah/eynollah.py | 46 +++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2fe7325..72a72d9 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -541,6 +541,7 @@ class Eynollah: img = self.imread() _, page_coord = self.early_page_for_num_of_column_classification(img) + if not self.dir_in: model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) if self.input_binary: @@ -611,6 +612,10 @@ class Eynollah: width_early = img.shape[1] t1 = time.time() _, page_coord = self.early_page_for_num_of_column_classification(img_bin) + + self.image_page_org_size = img[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3], :] + self.page_coord = page_coord + if not self.dir_in: model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) @@ -737,7 +742,7 @@ class Eynollah: def get_image_and_scales_after_enhancing(self, img_org, img_res): self.logger.debug("enter get_image_and_scales_after_enhancing") self.image = np.copy(img_res) - self.image = self.image.astype(np.uint8) + #self.image = self.image.astype(np.uint8) self.image_org = np.copy(img_org) self.height_org = self.image_org.shape[0] self.width_org = self.image_org.shape[1] @@ -1059,19 +1064,18 @@ class Eynollah: if not patches: img_h_page = img.shape[0] img_w_page = img.shape[1] - img = img / float(255.0) + img = img / 255.0 img = resize_image(img, img_height_model, img_width_model) label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), verbose=0) - - #seg_not_base = label_p_pred[0,:,:,4] - - #seg_not_base[seg_not_base>0.4] =1 - #seg_not_base[seg_not_base<1] =0 - seg = np.argmax(label_p_pred, axis=3)[0] - #seg[seg_not_base==1]=4 + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[0,:,:,4] + seg_art[seg_art<0.1] =0 + seg_art[seg_art>0] =1 + seg[seg_art==1]=4 + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) @@ -2151,7 +2155,7 @@ class Eynollah: #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: - img_w_new = 1000 + img_w_new = 800 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: @@ -2206,29 +2210,39 @@ class Eynollah: textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) + + #print(self.image_org.shape) + + #plt.imshwo(self.image_page_org_size) + #plt.show() if not skip_layout_and_reading_order: #print("inside 2 ", time.time()-t_in) - #print(img_resized.shape, num_col_classifier, "num_col_classifier") if not self.dir_in: if num_col_classifier == 1 or num_col_classifier == 2: + prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, model_region, n_batch_inference=1) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = False) + prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light, n_batch_inference=3) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: if num_col_classifier == 1 or num_col_classifier == 2: - prediction_regions_org = self.do_prediction_new_concept(False, img_resized, self.model_region_1_2, n_batch_inference=1) + prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=False) + prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) + #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() + prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) @@ -3195,7 +3209,7 @@ class Eynollah: scale = 1 if is_image_enhanced: if self.allow_enhancement: - img_res = img_res.astype(np.uint8) + #img_res = img_res.astype(np.uint8) self.get_image_and_scales(img_org, img_res, scale) if self.plotter: self.plotter.save_enhanced_image(img_res) From 1774076f4a9536ae68d9ab0a982bb84f65c8d858 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 30 Sep 2024 16:10:29 +0200 Subject: [PATCH 251/412] updating light version. Remove textlines or textregion contours inside a bigger one --- qurator/eynollah/eynollah.py | 124 ++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 10 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 72a72d9..cbc7b88 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylay12sp_0_2"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -1071,8 +1071,13 @@ class Eynollah: seg = np.argmax(label_p_pred, axis=3)[0] if thresholding_for_artificial_class_in_light_version: + #seg_text = label_p_pred[0,:,:,1] + #seg_text[seg_text<0.2] =0 + #seg_text[seg_text>0] =1 + #seg[seg_text==1]=1 + seg_art = label_p_pred[0,:,:,4] - seg_art[seg_art<0.1] =0 + seg_art[seg_art<0.2] =0 seg_art[seg_art>0] =1 seg[seg_art==1]=4 @@ -2159,7 +2164,7 @@ class Eynollah: img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: - img_w_new = 1300#1500 + img_w_new = 1500#1500 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 3: @@ -2222,7 +2227,7 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = False) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) @@ -2232,7 +2237,7 @@ class Eynollah: else: if num_col_classifier == 1 or num_col_classifier == 2: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=False) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) @@ -2249,16 +2254,19 @@ class Eynollah: img_bin = resize_image(img_bin,img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] + mask_lines_only = (prediction_regions_org[:,:] ==3)*1 + + mask_texts_only = (prediction_regions_org[:,:] ==1)*1 mask_texts_only = mask_texts_only.astype('uint8') - ##if num_col_classifier == 1 or num_col_classifier == 2: - ###mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) - ##mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) + #if num_col_classifier == 1 or num_col_classifier == 2: + #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + #mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_texts_only = cv2.dilate(mask_texts_only, kernel=np.ones((2,2), np.uint8), iterations=1) @@ -4110,6 +4118,7 @@ class Eynollah: if dilation_m1<6: dilation_m1 = 6 #print(dilation_m1, 'dilation_m1') + dilation_m1 = 5 dilation_m2 = int(dilation_m1/2.) +1 for i in range(len(x_differential)): @@ -4267,7 +4276,6 @@ class Eynollah: for ij in range(len(all_found_textline_polygons[j])): con_ind = all_found_textline_polygons[j][ij] - print(len(con_ind[:,0,0]),'con_ind[:,0,0]') area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) @@ -4303,7 +4311,7 @@ class Eynollah: if dilation_m1<4: dilation_m1 = 4 #print(dilation_m1, 'dilation_m1') - dilation_m2 = int(dilation_m1/2.) +1 + dilation_m2 = int(dilation_m1/2.) +1 for i in range(len(x_differential)): if abs_diff[i]==0: @@ -4339,6 +4347,100 @@ class Eynollah: all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons + def filter_contours_inside_a_bigger_one(self,contours, image, marginal_cnts=None, type_contour="textregion"): + if type_contour=="textregion": + areas = [cv2.contourArea(contours[j]) for j in range(len(contours))] + area_tot = image.shape[0]*image.shape[1] + + M_main = [cv2.moments(contours[j]) for j in range(len(contours))] + cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + + areas_ratio = np.array(areas)/ area_tot + contours_index_small = [ind for ind in range(len(contours)) if areas_ratio[ind] < 1e-3] + contours_index_big = [ind for ind in range(len(contours)) if areas_ratio[ind] >= 1e-3] + + #contours_> = [contours[ind] for ind in contours_index_big] + indexes_to_be_removed = [] + for ind_small in contours_index_small: + results = [cv2.pointPolygonTest(contours[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in contours_index_big ] + if marginal_cnts: + results_marginal = [cv2.pointPolygonTest(marginal_cnts[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in range(len(marginal_cnts)) ] + results_marginal = np.array(results_marginal) + + if np.any(results_marginal==1): + indexes_to_be_removed.append(ind_small) + + results = np.array(results) + + if np.any(results==1): + indexes_to_be_removed.append(ind_small) + + + if len(indexes_to_be_removed)>0: + indexes_to_be_removed = np.unique(indexes_to_be_removed) + for ind in indexes_to_be_removed: + contours.pop(ind) + return contours + + + else: + contours_txtline_of_all_textregions = [] + + for jj in range(len(contours)): + contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours[jj] + + M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] + cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] + cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] + + areas_tot = [cv2.contourArea(con_ind) for con_ind in contours_txtline_of_all_textregions] + area_tot_tot = image.shape[0]*image.shape[1] + + areas_ratio_tot = np.array(areas_tot)/ area_tot_tot + + contours_index_big_tot = [ind for ind in range(len(contours_txtline_of_all_textregions)) if areas_ratio_tot[ind] >= 1e-2] + + + for jj in range(len(contours)): + contours_in = contours[jj] + #print(len(contours_in)) + areas = [cv2.contourArea(con_ind) for con_ind in contours_in] + area_tot = image.shape[0]*image.shape[1] + + M_main = [cv2.moments(contours_in[j]) for j in range(len(contours_in))] + cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + + areas_ratio = np.array(areas)/ area_tot + + if len(areas_ratio)>=1: + #print(np.max(areas_ratio), np.min(areas_ratio)) + contours_index_small = [ind for ind in range(len(contours_in)) if areas_ratio[ind] < 1e-2] + #contours_index_big = [ind for ind in range(len(contours_in)) if areas_ratio[ind] >= 1e-3] + + if len(contours_index_small)>0: + indexes_to_be_removed = [] + for ind_small in contours_index_small: + results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in contours_index_big_tot ] + + results = np.array(results) + + if np.any(results==1): + indexes_to_be_removed.append(ind_small) + + + if len(indexes_to_be_removed)>0: + indexes_to_be_removed = np.unique(indexes_to_be_removed) + + for ind in indexes_to_be_removed: + contours[jj].pop(ind) + + return contours + + + + def dilate_textlines(self,all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): for i in range(len(all_found_textline_polygons[j])): @@ -4725,6 +4827,7 @@ class Eynollah: #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) #txt_con_org = self.dilate_textregions_contours(txt_con_org) #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) @@ -4742,6 +4845,7 @@ class Eynollah: #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") all_found_textline_polygons_marginals = self.dilate_textline_contours(all_found_textline_polygons_marginals) else: From ab63d5ba408a3dfe42ee897b5e6976d4fc501bdd Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 30 Sep 2024 21:28:39 +0200 Subject: [PATCH 252/412] updating light version features --- qurator/eynollah/eynollah.py | 105 +++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index cbc7b88..61289fa 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2225,10 +2225,13 @@ class Eynollah: if not self.dir_in: if num_col_classifier == 1 or num_col_classifier == 2: - prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) - prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page + if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + else: + prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) @@ -2236,9 +2239,12 @@ class Eynollah: ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: if num_col_classifier == 1 or num_col_classifier == 2: - prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) - prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page + if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) + else: + prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) + prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) @@ -4356,6 +4362,8 @@ class Eynollah: cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + + areas_ratio = np.array(areas)/ area_tot contours_index_small = [ind for ind in range(len(contours)) if areas_ratio[ind] < 1e-3] contours_index_big = [ind for ind in range(len(contours)) if areas_ratio[ind] >= 1e-3] @@ -4379,64 +4387,75 @@ class Eynollah: if len(indexes_to_be_removed)>0: indexes_to_be_removed = np.unique(indexes_to_be_removed) + indexes_to_be_removed = np.sort(indexes_to_be_removed)[::-1] for ind in indexes_to_be_removed: contours.pop(ind) + return contours else: contours_txtline_of_all_textregions = [] + indexes_of_textline_tot = [] + index_textline_inside_textregion = [] for jj in range(len(contours)): contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours[jj] + ind_ins = np.zeros( len(contours[jj]) ) + jj + list_ind_ins = list(ind_ins) + + ind_textline_inside_tr = np.array (range(len(contours[jj])) ) + + list_ind_textline_inside_tr = list(ind_textline_inside_tr) + + index_textline_inside_textregion = index_textline_inside_textregion + list_ind_textline_inside_tr + + indexes_of_textline_tot = indexes_of_textline_tot + list_ind_ins + + M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] + areas_tot = [cv2.contourArea(con_ind) for con_ind in contours_txtline_of_all_textregions] area_tot_tot = image.shape[0]*image.shape[1] - areas_ratio_tot = np.array(areas_tot)/ area_tot_tot - - contours_index_big_tot = [ind for ind in range(len(contours_txtline_of_all_textregions)) if areas_ratio_tot[ind] >= 1e-2] - - - for jj in range(len(contours)): - contours_in = contours[jj] - #print(len(contours_in)) - areas = [cv2.contourArea(con_ind) for con_ind in contours_in] - area_tot = image.shape[0]*image.shape[1] + textregion_index_to_del = [] + textline_in_textregion_index_to_del = [] + for ij in range(len(contours_txtline_of_all_textregions)): - M_main = [cv2.moments(contours_in[j]) for j in range(len(contours_in))] - cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] - cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + args_all = list(np.array(range(len(contours_txtline_of_all_textregions)))) - areas_ratio = np.array(areas)/ area_tot + args_all.pop(ij) - if len(areas_ratio)>=1: - #print(np.max(areas_ratio), np.min(areas_ratio)) - contours_index_small = [ind for ind in range(len(contours_in)) if areas_ratio[ind] < 1e-2] - #contours_index_big = [ind for ind in range(len(contours_in)) if areas_ratio[ind] >= 1e-3] - - if len(contours_index_small)>0: - indexes_to_be_removed = [] - for ind_small in contours_index_small: - results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in contours_index_big_tot ] - - results = np.array(results) + areas_without = np.array(areas_tot)[args_all] + area_of_con_interest = areas_tot[ij] + + args_with_bigger_area = np.array(args_all)[areas_without > area_of_con_interest] + + if len(args_with_bigger_area)>0: + results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main_tot[ij], cy_main_tot[ij]), False) for ind in args_with_bigger_area ] + results = np.array(results) + if np.any(results==1): + #print(indexes_of_textline_tot[ij], index_textline_inside_textregion[ij]) + textregion_index_to_del.append(int(indexes_of_textline_tot[ij])) + textline_in_textregion_index_to_del.append(int(index_textline_inside_textregion[ij])) + #contours[int(indexes_of_textline_tot[ij])].pop(int(index_textline_inside_textregion[ij])) - if np.any(results==1): - indexes_to_be_removed.append(ind_small) - - - if len(indexes_to_be_removed)>0: - indexes_to_be_removed = np.unique(indexes_to_be_removed) - - for ind in indexes_to_be_removed: - contours[jj].pop(ind) - - return contours + uniqe_args_trs = np.unique(textregion_index_to_del) + + for ind_u_a_trs in uniqe_args_trs: + textline_in_textregion_index_to_del_ind = np.array(textline_in_textregion_index_to_del)[np.array(textregion_index_to_del)==ind_u_a_trs] + textline_in_textregion_index_to_del_ind = np.sort(textline_in_textregion_index_to_del_ind)[::-1] + + for ittrd in textline_in_textregion_index_to_del_ind: + contours[ind_u_a_trs].pop(ittrd) + + return contours + + @@ -4852,6 +4871,8 @@ class Eynollah: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) From c487be2a1dfcb444f1896dd725183b6dfc8fb96f Mon Sep 17 00:00:00 2001 From: kba Date: Tue, 1 Oct 2024 15:38:01 +0200 Subject: [PATCH 253/412] dockerfile: use src-layout --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6c76564..6780bc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ ENV PYTHONIOENCODING=utf8 ENV XDG_DATA_HOME=/usr/local/share WORKDIR /build-eynollah -COPY qurator/ ./qurator +COPY src/ ./src COPY pyproject.toml . COPY requirements.txt . COPY README.md . From b13759fdcf50db60966ec98050fa95bddb54728a Mon Sep 17 00:00:00 2001 From: kba Date: Tue, 1 Oct 2024 15:38:39 +0200 Subject: [PATCH 254/412] ci: smoke-test make docker --- .github/workflows/test-eynollah.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 98ddc06..3a33dcf 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -34,3 +34,5 @@ jobs: pip install -r requirements-test.txt - name: Test with pytest run: make test + - name: Test docker build + run: make docker From 543ed4bc38b94acf53f48a9224b97322cade0e5b Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 2 Oct 2024 14:09:13 +0200 Subject: [PATCH 255/412] -light version need -tll to be enabled otherwise the process will be ended. --- qurator/eynollah/cli.py | 3 ++ qurator/eynollah/eynollah.py | 63 +++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index b293403..4c762a8 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -227,6 +227,9 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s if textline_light and not light_version: print('Error: You used -tll to enable light textline detection but -light is not enabled') sys.exit(1) + if light_version and not textline_light: + print('Error: You used -light without -tll. Light version need light textline to be enabled.') + sys.exit(1) eynollah = Eynollah( image_filename=image, dir_out=out, diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 61289fa..6b8193c 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlylay12sp_0_2"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlyla_12_0_2_con_18_22"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -1055,6 +1055,35 @@ class Eynollah: #del model #gc.collect() return prediction_true + def do_padding_with_scale(self,img, scale): + h_n = int(img.shape[0]*scale) + w_n = int(img.shape[1]*scale) + + channel0_avg = int( np.mean(img[:,:,0]) ) + channel1_avg = int( np.mean(img[:,:,1]) ) + channel2_avg = int( np.mean(img[:,:,2]) ) + + h_diff = img.shape[0] - h_n + w_diff = img.shape[1] - w_n + + h_start = int(h_diff / 2.) + w_start = int(w_diff / 2.) + + img_res = resize_image(img, h_n, w_n) + #label_res = resize_image(label, h_n, w_n) + + img_scaled_padded = np.copy(img) + + #label_scaled_padded = np.zeros(label.shape) + + img_scaled_padded[:,:,0] = channel0_avg + img_scaled_padded[:,:,1] = channel1_avg + img_scaled_padded[:,:,2] = channel2_avg + + img_scaled_padded[h_start:h_start+h_n, w_start:w_start+w_n,:] = img_res[:,:,:] + #label_scaled_padded[h_start:h_start+h_n, w_start:w_start+w_n,:] = label_res[:,:,:] + + return img_scaled_padded#, label_scaled_padded def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction") @@ -4349,6 +4378,38 @@ class Eynollah: con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 + + con_ind = con_ind.astype(np.int32) + + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] + + results = np.array(results) + + results[results==0] = 1 + + + diff_result = np.diff(results) + + indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] + indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] + + if results[0]==1: + con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] + con_scaled[:indices_m2[0]+1,0, 0] = con_ind[:indices_m2[0]+1,0,0] + indices_m2 = indices_m2[1:] + + + + if len(indices_2)>len(indices_m2): + con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] + con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] + indices_2 = indices_2[:-1] + + + for ii in range(len(indices_2)): + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] + all_found_textline_polygons[j][ij][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons From 1da4b7f589af94beea75157b80c0a7ecb6a213de Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 7 Oct 2024 10:55:10 +0200 Subject: [PATCH 256/412] updating light version --- qurator/eynollah/eynollah.py | 41 ++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 6b8193c..2c14ab9 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_earlyla_12_0_2_con_18_22"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -2189,7 +2189,7 @@ class Eynollah: #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: - img_w_new = 800 + img_w_new = 1000 img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 2: @@ -2299,9 +2299,9 @@ class Eynollah: mask_texts_only = mask_texts_only.astype('uint8') - #if num_col_classifier == 1 or num_col_classifier == 2: - #mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) - #mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) + ##if num_col_classifier == 1 or num_col_classifier == 2: + ###mask_texts_only = cv2.erode(mask_texts_only, KERNEL, iterations=1) + ##mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_texts_only = cv2.dilate(mask_texts_only, kernel=np.ones((2,2), np.uint8), iterations=1) @@ -4153,7 +4153,7 @@ class Eynollah: if dilation_m1<6: dilation_m1 = 6 #print(dilation_m1, 'dilation_m1') - dilation_m1 = 5 + dilation_m1 = 6 dilation_m2 = int(dilation_m1/2.) +1 for i in range(len(x_differential)): @@ -4657,6 +4657,31 @@ class Eynollah: all_found_textline_polygons[j][i][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons + + def delete_regions_without_textlines(self,slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con): + slopes_rem = [] + all_found_textline_polygons_rem = [] + boxes_text_rem = [] + txt_con_org_rem = [] + contours_only_text_parent_rem = [] + index_by_text_par_con_rem = [] + + for i, ind_con in enumerate(all_found_textline_polygons): + if len(ind_con): + all_found_textline_polygons_rem.append(ind_con) + slopes_rem.append(slopes[i]) + boxes_text_rem.append(boxes_text[i]) + txt_con_org_rem.append(txt_con_org[i]) + contours_only_text_parent_rem.append(contours_only_text_parent[i]) + index_by_text_par_con_rem.append(index_by_text_par_con[i]) + + index_sort = np.argsort(index_by_text_par_con_rem) + indexes_new = np.array(range(len(index_by_text_par_con_rem))) + + index_by_text_par_con_rem_sort = [indexes_new[index_sort==j][0] for j in range(len(index_by_text_par_con_rem))] + + return slopes_rem, all_found_textline_polygons_rem, boxes_text_rem, txt_con_org_rem, contours_only_text_parent_rem, index_by_text_par_con_rem_sort + def run(self): """ Get image and scales, then extract the page of scanned image @@ -4923,6 +4948,9 @@ class Eynollah: slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") @@ -5121,6 +5149,7 @@ class Eynollah: all_found_textline_polygons=[ all_found_textline_polygons ] all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") order_text_new = [0] From 21893910b87c6546bbd68e8b4dd720791a24589c Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:20:53 +0200 Subject: [PATCH 257/412] relax tf2 requirement to < 2.13 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f01d319..e6f6e4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 -tensorflow == 2.12.1 +tensorflow < 2.13 imutils >= 0.5.3 matplotlib setuptools >= 50 From bc9dddd2c09190f975b5b7fce8d2f5e74aaf2f97 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Wed, 16 Oct 2024 14:21:48 +0200 Subject: [PATCH 258/412] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 292cfbc..916c556 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ :warning: Development is currently focused on achieving the best possible quality of results for a wide variety of historical documents and therefore processing can be very slow. We aim to improve this, but contributions are welcome. ## Installation -Python `3.8-3.11` with Tensorflow `2.12-2.15` on Linux are currently supported. +Python `3.8-3.11` with Tensorflow `<2.13` on Linux are currently supported. For (limited) GPU support the CUDA toolkit needs to be installed. From 3ef4eac24ca5d876243c62860ad9d4fa05110081 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 17 Oct 2024 19:12:28 +0200 Subject: [PATCH 259/412] textlines of textregions are extracted in a faster way + early layout for all documents is done with no patches model and on rgb input --- qurator/eynollah/eynollah.py | 120 +++++++++++++++++++--------- qurator/eynollah/utils/marginals.py | 65 ++------------- 2 files changed, 89 insertions(+), 96 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 2c14ab9..fd66b81 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -252,7 +252,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: @@ -1710,6 +1710,36 @@ class Eynollah: self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 + def get_slopes_and_deskew_new_light2(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): + + polygons_of_textlines = return_contours_of_interested_region(textline_mask_tot,1,0.00001) + + M_main_tot = [cv2.moments(polygons_of_textlines[j]) for j in range(len(polygons_of_textlines))] + cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] + cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] + + args_textlines = np.array(range(len(polygons_of_textlines))) + all_found_textline_polygons = [] + slopes = [] + all_box_coord =[] + + for index, con_region_ind in enumerate(contours_par): + results = [cv2.pointPolygonTest(con_region_ind, (cx_main_tot[ind], cy_main_tot[ind]), False) for ind in args_textlines ] + results = np.array(results) + + indexes_in = args_textlines[results==1] + + textlines_ins = [polygons_of_textlines[ind] for ind in indexes_in] + + all_found_textline_polygons.append(textlines_ins) + slopes.append(0) + + _, crop_coor = crop_image_inside_box(boxes[index],image_page_rotated) + + all_box_coord.append(crop_coor) + + return slopes, all_found_textline_polygons, boxes, contours, contours_par, all_box_coord, np.array(range(len(contours_par))) + def get_slopes_and_deskew_new_light(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): self.logger.debug("enter get_slopes_and_deskew_new") if len(contours)>15: @@ -2099,14 +2129,14 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.2, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) @@ -2216,14 +2246,14 @@ class Eynollah: #if (not self.input_binary) or self.full_layout: #if self.input_binary: #img_bin = np.copy(img_resized) - if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 3): + if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 30): if not self.dir_in: model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) else: prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) - #print("inside bin ", time.time()-t_bin) + print("inside bin ", time.time()-t_bin) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 @@ -2236,7 +2266,7 @@ class Eynollah: else: img_bin = np.copy(img_resized) - #print("inside 1 ", time.time()-t_in) + print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) textline_mask_tot_ea = self.run_textline(img_resized, num_col_classifier) @@ -2246,14 +2276,15 @@ class Eynollah: #print(self.image_org.shape) + #cv2.imwrite('out_13.png', self.image_page_org_size) #plt.imshwo(self.image_page_org_size) #plt.show() if not skip_layout_and_reading_order: - #print("inside 2 ", time.time()-t_in) + print("inside 2 ", time.time()-t_in) if not self.dir_in: - if num_col_classifier == 1 or num_col_classifier == 2: + if num_col_classifier == 1 or num_col_classifier >= 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) @@ -2267,7 +2298,7 @@ class Eynollah: ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: - if num_col_classifier == 1 or num_col_classifier == 2: + if num_col_classifier == 1 or num_col_classifier >= 2: if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) else: @@ -2278,7 +2309,7 @@ class Eynollah: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) - #print("inside 3 ", time.time()-t_in) + print("inside 3 ", time.time()-t_in) #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() @@ -2356,7 +2387,15 @@ class Eynollah: text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - #print("inside 4 ", time.time()-t_in) + + #plt.imshow(textline_mask_tot_ea) + #plt.show() + + textline_mask_tot_ea[(text_regions_p_true==0) | (text_regions_p_true==4) ] = 0 + + #plt.imshow(textline_mask_tot_ea) + #plt.show() + print("inside 4 ", time.time()-t_in) return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin else: img_bin = resize_image(img_bin,img_height_h, img_width_h ) @@ -3308,7 +3347,7 @@ class Eynollah: if self.tables: regions_without_separators[table_prediction==1] = 1 regions_without_separators = regions_without_separators.astype(np.uint8) - text_regions_p = get_marginals(rotate_image(regions_without_separators, slope_deskew), text_regions_p, num_col_classifier, slope_deskew, kernel=KERNEL) + text_regions_p = get_marginals(rotate_image(regions_without_separators, slope_deskew), text_regions_p, num_col_classifier, slope_deskew, light_version=self.light_version, kernel=KERNEL) except Exception as e: self.logger.error("exception %s", e) @@ -3319,6 +3358,7 @@ class Eynollah: def run_boxes_no_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts): self.logger.debug('enter run_boxes_no_full_layout') + t_0_box = time.time() if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) @@ -3328,6 +3368,7 @@ class Eynollah: if self.tables: regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 regions_without_separators = (text_regions_p[:, :] == 1) * 1 # ( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) + print(time.time()-t_0_box,'time box in 1') if self.tables: regions_without_separators[table_prediction ==1 ] = 1 if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -3340,7 +3381,7 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - + print(time.time()-t_0_box,'time box in 2') self.logger.info("num_col_classifier: %s", num_col_classifier) if num_col_classifier >= 3: @@ -3350,6 +3391,7 @@ class Eynollah: else: regions_without_separators_d = regions_without_separators_d.astype(np.uint8) regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + print(time.time()-t_0_box,'time box in 3') t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) @@ -3378,7 +3420,7 @@ class Eynollah: img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) - + print(time.time()-t_0_box,'time box in 4') self.logger.info("detecting boxes took %.1fs", time.time() - t1) if self.tables: @@ -3410,7 +3452,7 @@ class Eynollah: pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) - + print(time.time()-t_0_box,'time box in 5') self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables @@ -3751,8 +3793,10 @@ class Eynollah: img_poly[text_regions_p[:,:]==3] = 4 img_poly[text_regions_p[:,:]==6] = 5 - - model_ro_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) + if self.dir_in: + pass + else: + self.model_reading_order_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) height1 =672#448 width1 = 448#224 @@ -3793,7 +3837,7 @@ class Eynollah: img3 = img3.astype(np.uint16) - inference_bs = 4 + inference_bs = 3 input_1= np.zeros( (inference_bs, height1, width1,3)) starting_list_of_regions = [] starting_list_of_regions.append( list(range(labels_con.shape[2])) ) @@ -3835,7 +3879,7 @@ class Eynollah: batch_counter = batch_counter+1 if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): - y_pr=model_ro_machine.predict(input_1 , verbose=0) + y_pr=self.model_reading_order_machine.predict(input_1 , verbose=0) if batch_counter==inference_bs: iteration_batches = inference_bs @@ -4698,16 +4742,16 @@ class Eynollah: t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) - #print("text region early -11 in %.1fs", time.time() - t0) + print("text region early -11 in %.1fs", time.time() - t0) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) - #print("text region early -1 in %.1fs", time.time() - t0) + print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() if not self.skip_layout_and_reading_order: if self.light_version: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) + print("text region early -2 in %.1fs", time.time() - t0) if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: @@ -4720,17 +4764,17 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) + slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea) + print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) + print("text region early -3 in %.1fs", time.time() - t0) textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) + print("text region early -4 in %.1fs", time.time() - t0) else: text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) self.logger.info("Textregion detection took %.1fs ", time.time() - t1) @@ -4751,7 +4795,7 @@ class Eynollah: continue else: return pcgts - #print("text region early in %.1fs", time.time() - t0) + print("text region early in %.1fs", time.time() - t0) t1 = time.time() if not self.light_version: textline_mask_tot_ea = self.run_textline(image_page) @@ -4793,7 +4837,8 @@ class Eynollah: image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) + print("text region early 2 marginal in %.1fs", time.time() - t0) + ## birdan sora chock chakir t1 = time.time() if not self.full_layout: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) @@ -4807,7 +4852,7 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - #print("text region early 2 in %.1fs", time.time() - t0) + print("text region early 2 in %.1fs", time.time() - t0) ###min_con_area = 0.000005 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text, hir_on_text = return_contours_of_image(text_only) @@ -4929,7 +4974,7 @@ class Eynollah: else: pass - #print("text region early 3 in %.1fs", time.time() - t0) + print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) @@ -4938,14 +4983,17 @@ class Eynollah: #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) + print("text region early 4 in %.1fs", time.time() - t0) boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) + print("text region early 5 in %.1fs", time.time() - t0) + ## birdan sora chock chakir if not self.curved_line: if self.light_version: if self.textline_light: - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) @@ -4974,7 +5022,7 @@ class Eynollah: all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) + print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) @@ -5134,7 +5182,7 @@ class Eynollah: self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) + print("text region early 7 in %.1fs", time.time() - t0) else: _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) diff --git a/qurator/eynollah/utils/marginals.py b/qurator/eynollah/utils/marginals.py index 7c43de6..984156f 100644 --- a/qurator/eynollah/utils/marginals.py +++ b/qurator/eynollah/utils/marginals.py @@ -8,7 +8,7 @@ from .contour import find_new_features_of_contours, return_contours_of_intereste from .resize import resize_image from .rotate import rotate_image -def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=None): +def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, light_version=False, kernel=None): mask_marginals=np.zeros((text_with_lines.shape[0],text_with_lines.shape[1])) mask_marginals=mask_marginals.astype(np.uint8) @@ -49,27 +49,14 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N if thickness_along_y_percent>=14: text_with_lines_y_rev=-1*text_with_lines_y[:] - #print(text_with_lines_y) - #print(text_with_lines_y_rev) - - - - - #plt.plot(text_with_lines_y) - #plt.show() - text_with_lines_y_rev=text_with_lines_y_rev-np.min(text_with_lines_y_rev) - #plt.plot(text_with_lines_y_rev) - #plt.show() sigma_gaus=1 region_sum_0= gaussian_filter1d(text_with_lines_y, sigma_gaus) region_sum_0_rev=gaussian_filter1d(text_with_lines_y_rev, sigma_gaus) - #plt.plot(region_sum_0_rev) - #plt.show() region_sum_0_updown=region_sum_0[len(region_sum_0)::-1] first_nonzero=(next((i for i, x in enumerate(region_sum_0) if x), None)) @@ -78,43 +65,17 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N last_nonzero=len(region_sum_0)-last_nonzero - ##img_sum_0_smooth_rev=-region_sum_0 - - mid_point=(last_nonzero+first_nonzero)/2. one_third_right=(last_nonzero-mid_point)/3.0 one_third_left=(mid_point-first_nonzero)/3.0 - #img_sum_0_smooth_rev=img_sum_0_smooth_rev-np.min(img_sum_0_smooth_rev) - - - - peaks, _ = find_peaks(text_with_lines_y_rev, height=0) - - peaks=np.array(peaks) - - - #print(region_sum_0[peaks]) - ##plt.plot(region_sum_0) - ##plt.plot(peaks,region_sum_0[peaks],'*') - ##plt.show() - #print(first_nonzero,last_nonzero,peaks) peaks=peaks[(peaks>first_nonzero) & ((peaksmid_point] @@ -137,9 +98,6 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N - - #print(point_left,point_right) - #print(text_regions.shape) if point_right>=mask_marginals.shape[1]: point_right=mask_marginals.shape[1]-1 @@ -148,10 +106,8 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N except: mask_marginals[:,:]=1 - #print(mask_marginals.shape,point_left,point_right,'nadosh') mask_marginals_rotated=rotate_image(mask_marginals,-slope_deskew) - #print(mask_marginals_rotated.shape,'nadosh') mask_marginals_rotated_sum=mask_marginals_rotated.sum(axis=0) mask_marginals_rotated_sum[mask_marginals_rotated_sum!=0]=1 @@ -168,11 +124,6 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N max_point_of_right_marginal=text_regions.shape[1]-1 - #print(np.min(index_x_interest) ,np.max(index_x_interest),'minmaxnew') - #print(mask_marginals_rotated.shape,text_regions.shape,'mask_marginals_rotated') - #plt.imshow(mask_marginals) - #plt.show() - #plt.imshow(mask_marginals_rotated) #plt.show() @@ -195,10 +146,9 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N x_min_marginals_right=[] for i in range(len(cx_text_only)): - x_width_mar=abs(x_min_text_only[i]-x_max_text_only[i]) y_height_mar=abs(y_min_text_only[i]-y_max_text_only[i]) - #print(x_width_mar,y_height_mar,y_height_mar/x_width_mar,'y_height_mar') + if x_width_mar>16 and y_height_mar/x_width_mar<18: marginlas_should_be_main_text.append(polygons_of_marginals[i]) if x_min_text_only[i]<(mid_point-one_third_left): @@ -220,18 +170,13 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, kernel=N x_min_marginals_right=[text_regions.shape[1]-1] - - - #print(x_min_marginals_left[0],x_min_marginals_right[0],'margo') - - #print(marginlas_should_be_main_text,'marginlas_should_be_main_text') text_regions=cv2.fillPoly(text_regions, pts =marginlas_should_be_main_text, color=(4,4)) - #print(np.unique(text_regions)) #text_regions[:,:int(x_min_marginals_left[0])][text_regions[:,:int(x_min_marginals_left[0])]==1]=0 #text_regions[:,int(x_min_marginals_right[0]):][text_regions[:,int(x_min_marginals_right[0]):]==1]=0 - + + text_regions[:,:int(min_point_of_left_marginal)][text_regions[:,:int(min_point_of_left_marginal)]==1]=0 text_regions[:,int(max_point_of_right_marginal):][text_regions[:,int(max_point_of_right_marginal):]==1]=0 From f93fa12441104324ee8e7ced0488b44827704de3 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 18 Oct 2024 09:14:42 +0200 Subject: [PATCH 260/412] doing more multiprocessing in order to make the process faster --- qurator/eynollah/eynollah.py | 92 +++--- qurator/eynollah/utils/__init__.py | 93 +----- qurator/eynollah/utils/contour.py | 73 ++++- qurator/eynollah/utils/separate_lines.py | 386 +++++++++++++++++------ 4 files changed, 407 insertions(+), 237 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index fd66b81..79724cc 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2253,7 +2253,7 @@ class Eynollah: else: prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) - print("inside bin ", time.time()-t_bin) + #print("inside bin ", time.time()-t_bin) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 @@ -2266,7 +2266,7 @@ class Eynollah: else: img_bin = np.copy(img_resized) - print("inside 1 ", time.time()-t_in) + #print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) textline_mask_tot_ea = self.run_textline(img_resized, num_col_classifier) @@ -2281,7 +2281,7 @@ class Eynollah: #plt.imshwo(self.image_page_org_size) #plt.show() if not skip_layout_and_reading_order: - print("inside 2 ", time.time()-t_in) + #print("inside 2 ", time.time()-t_in) if not self.dir_in: if num_col_classifier == 1 or num_col_classifier >= 2: @@ -2309,7 +2309,7 @@ class Eynollah: prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) - print("inside 3 ", time.time()-t_in) + #print("inside 3 ", time.time()-t_in) #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() @@ -2395,7 +2395,7 @@ class Eynollah: #plt.imshow(textline_mask_tot_ea) #plt.show() - print("inside 4 ", time.time()-t_in) + #print("inside 4 ", time.time()-t_in) return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin else: img_bin = resize_image(img_bin,img_height_h, img_width_h ) @@ -3368,7 +3368,7 @@ class Eynollah: if self.tables: regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 regions_without_separators = (text_regions_p[:, :] == 1) * 1 # ( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) - print(time.time()-t_0_box,'time box in 1') + #print(time.time()-t_0_box,'time box in 1') if self.tables: regions_without_separators[table_prediction ==1 ] = 1 if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -3381,7 +3381,7 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - print(time.time()-t_0_box,'time box in 2') + #print(time.time()-t_0_box,'time box in 2') self.logger.info("num_col_classifier: %s", num_col_classifier) if num_col_classifier >= 3: @@ -3391,36 +3391,41 @@ class Eynollah: else: regions_without_separators_d = regions_without_separators_d.astype(np.uint8) regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - print(time.time()-t_0_box,'time box in 3') + #print(time.time()-t_0_box,'time box in 3') t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) + #print(time.time()-t_0_box,'time box in 3.1') - text_regions_p_tables = np.copy(text_regions_p) - text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) - img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) + if self.tables: + text_regions_p_tables = np.copy(text_regions_p) + text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) + #print(time.time()-t_0_box,'time box in 3.2') + img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) + #print(time.time()-t_0_box,'time box in 3.3') else: boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) - text_regions_p_tables = np.copy(text_regions_p_1_n) - text_regions_p_tables =np.round(text_regions_p_tables) - text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 - - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) - img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) - - img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) - img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) - img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) - img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) - print(time.time()-t_0_box,'time box in 4') + if self.tables: + text_regions_p_tables = np.copy(text_regions_p_1_n) + text_regions_p_tables =np.round(text_regions_p_tables) + text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 + + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) + + img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) + img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) + img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) + img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) + #print(time.time()-t_0_box,'time box in 4') self.logger.info("detecting boxes took %.1fs", time.time() - t1) if self.tables: @@ -3452,7 +3457,7 @@ class Eynollah: pixel_img = 10 contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) - print(time.time()-t_0_box,'time box in 5') + #print(time.time()-t_0_box,'time box in 5') self.logger.debug('exit run_boxes_no_full_layout') return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables @@ -4742,16 +4747,16 @@ class Eynollah: t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) - print("text region early -11 in %.1fs", time.time() - t0) + #print("text region early -11 in %.1fs", time.time() - t0) img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) - print("text region early -1 in %.1fs", time.time() - t0) + #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() if not self.skip_layout_and_reading_order: if self.light_version: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - print("text region early -2 in %.1fs", time.time() - t0) + #print("text region early -2 in %.1fs", time.time() - t0) if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: @@ -4764,17 +4769,17 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea) - print("text region early -2,5 in %.1fs", time.time() - t0) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) #self.logger.info("run graphics %.1fs ", time.time() - t1t) - print("text region early -3 in %.1fs", time.time() - t0) + #print("text region early -3 in %.1fs", time.time() - t0) textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - print("text region early -4 in %.1fs", time.time() - t0) + #print("text region early -4 in %.1fs", time.time() - t0) else: text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) self.logger.info("Textregion detection took %.1fs ", time.time() - t1) @@ -4795,7 +4800,7 @@ class Eynollah: continue else: return pcgts - print("text region early in %.1fs", time.time() - t0) + #print("text region early in %.1fs", time.time() - t0) t1 = time.time() if not self.light_version: textline_mask_tot_ea = self.run_textline(image_page) @@ -4837,7 +4842,7 @@ class Eynollah: image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) self.logger.info("detection of marginals took %.1fs", time.time() - t1) - print("text region early 2 marginal in %.1fs", time.time() - t0) + #print("text region early 2 marginal in %.1fs", time.time() - t0) ## birdan sora chock chakir t1 = time.time() if not self.full_layout: @@ -4852,7 +4857,7 @@ class Eynollah: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - print("text region early 2 in %.1fs", time.time() - t0) + #print("text region early 2 in %.1fs", time.time() - t0) ###min_con_area = 0.000005 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text, hir_on_text = return_contours_of_image(text_only) @@ -4974,19 +4979,20 @@ class Eynollah: else: pass - print("text region early 3 in %.1fs", time.time() - t0) + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + #print("text region early 3.5 in %.1fs", time.time() - t0) txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) #txt_con_org = self.dilate_textregions_contours(txt_con_org) #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - print("text region early 4 in %.1fs", time.time() - t0) + #print("text region early 4 in %.1fs", time.time() - t0) boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - print("text region early 5 in %.1fs", time.time() - t0) + #print("text region early 5 in %.1fs", time.time() - t0) ## birdan sora chock chakir if not self.curved_line: if self.light_version: @@ -5022,7 +5028,7 @@ class Eynollah: all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - print("text region early 6 in %.1fs", time.time() - t0) + #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) @@ -5182,7 +5188,7 @@ class Eynollah: self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts - print("text region early 7 in %.1fs", time.time() - t0) + #print("text region early 7 in %.1fs", time.time() - t0) else: _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 8705ecf..6219df2 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -7,7 +7,7 @@ import cv2 import imutils from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d - +import time from .is_nan import isNaN from .contour import (contours_in_same_horizon, find_new_features_of_contours, @@ -1342,7 +1342,7 @@ def return_points_with_boundies(peaks_neg_fin, first_point, last_point): return peaks_neg_tot def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, pixel_lines, contours_h=None): - + t_ins_c0 = time.time() separators_closeup=( (region_pre_p[:,:,:]==pixel_lines))*1 separators_closeup[0:110,:,:]=0 @@ -1356,84 +1356,47 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, separators_closeup_new=np.zeros((separators_closeup.shape[0] ,separators_closeup.shape[1] )) - - - - ##_,separators_closeup_n=self.combine_hor_lines_and_delete_cross_points_and_get_lines_features_back(region_pre_p[:,:,0]) separators_closeup_n=np.copy(separators_closeup) separators_closeup_n=separators_closeup_n.astype(np.uint8) - ##plt.imshow(separators_closeup_n[:,:,0]) - ##plt.show() separators_closeup_n_binary=np.zeros(( separators_closeup_n.shape[0],separators_closeup_n.shape[1]) ) separators_closeup_n_binary[:,:]=separators_closeup_n[:,:,0] separators_closeup_n_binary[:,:][separators_closeup_n_binary[:,:]!=0]=1 - #separators_closeup_n_binary[:,:][separators_closeup_n_binary[:,:]==0]=255 - #separators_closeup_n_binary[:,:][separators_closeup_n_binary[:,:]==-255]=0 - - - #separators_closeup_n_binary=(separators_closeup_n_binary[:,:]==2)*1 - - #gray = cv2.cvtColor(separators_closeup_n, cv2.COLOR_BGR2GRAY) - - ### - - #print(separators_closeup_n_binary.shape) + gray_early=np.repeat(separators_closeup_n_binary[:, :, np.newaxis], 3, axis=2) gray_early=gray_early.astype(np.uint8) - #print(gray_early.shape,'burda') imgray_e = cv2.cvtColor(gray_early, cv2.COLOR_BGR2GRAY) - #print('burda2') ret_e, thresh_e = cv2.threshold(imgray_e, 0, 255, 0) - #print('burda3') contours_line_e,hierarchy_e=cv2.findContours(thresh_e,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - #slope_lines_e,dist_x_e, x_min_main_e ,x_max_main_e ,cy_main_e,slope_lines_org_e,y_min_main_e, y_max_main_e, cx_main_e=self.find_features_of_lines(contours_line_e) - slope_linese,dist_xe, x_min_maine ,x_max_maine ,cy_maine,slope_lines_orge,y_min_maine, y_max_maine, cx_maine=find_features_of_lines(contours_line_e) dist_ye=y_max_maine-y_min_maine - #print(y_max_maine-y_min_maine,'y') - #print(dist_xe,'x') args_e=np.array(range(len(contours_line_e))) args_hor_e=args_e[(dist_ye<=50) & (dist_xe>=3*dist_ye)] - #print(args_hor_e,'jidi',len(args_hor_e),'jilva') cnts_hor_e=[] for ce in args_hor_e: cnts_hor_e.append(contours_line_e[ce]) - #print(len(slope_linese),'lieee') figs_e=np.zeros(thresh_e.shape) figs_e=cv2.fillPoly(figs_e,pts=cnts_hor_e,color=(1,1,1)) - #plt.imshow(figs_e) - #plt.show() - - ### - separators_closeup_n_binary=cv2.fillPoly(separators_closeup_n_binary,pts=cnts_hor_e,color=(0,0,0)) gray = cv2.bitwise_not(separators_closeup_n_binary) gray=gray.astype(np.uint8) - - #plt.imshow(gray) - #plt.show() - - bw = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, \ cv2.THRESH_BINARY, 15, -2) - ##plt.imshow(bw[:,:]) - ##plt.show() - + horizontal = np.copy(bw) vertical = np.copy(bw) @@ -1451,16 +1414,7 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, horizontal = cv2.dilate(horizontal,kernel,iterations = 2) horizontal = cv2.erode(horizontal,kernel,iterations = 2) - - ### - #print(np.unique(horizontal),'uni') horizontal=cv2.fillPoly(horizontal,pts=cnts_hor_e,color=(255,255,255)) - ### - - - - #plt.imshow(horizontal) - #plt.show() rows = vertical.shape[0] verticalsize = rows // 30 @@ -1471,35 +1425,21 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, vertical = cv2.dilate(vertical, verticalStructure) vertical = cv2.dilate(vertical,kernel,iterations = 1) - # Show extracted vertical lines horizontal,special_separators=combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(vertical,horizontal,num_col_classifier) - - #plt.imshow(horizontal) - #plt.show() - #print(vertical.shape,np.unique(vertical),'verticalvertical') separators_closeup_new[:,:][vertical[:,:]!=0]=1 separators_closeup_new[:,:][horizontal[:,:]!=0]=1 - ##plt.imshow(separators_closeup_new) - ##plt.show() - ##separators_closeup_n vertical=np.repeat(vertical[:, :, np.newaxis], 3, axis=2) vertical=vertical.astype(np.uint8) - ##plt.plot(vertical[:,:,0].sum(axis=0)) - ##plt.show() - - #plt.plot(vertical[:,:,0].sum(axis=1)) - #plt.show() - imgray = cv2.cvtColor(vertical, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_line_vers,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) slope_lines,dist_x, x_min_main ,x_max_main ,cy_main,slope_lines_org,y_min_main, y_max_main, cx_main=find_features_of_lines(contours_line_vers) - #print(slope_lines,'vertical') + args=np.array( range(len(slope_lines) )) args_ver=args[slope_lines==1] dist_x_ver=dist_x[slope_lines==1] @@ -1512,9 +1452,6 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, len_y=separators_closeup.shape[0]/3.0 - #plt.imshow(horizontal) - #plt.show() - horizontal=np.repeat(horizontal[:, :, np.newaxis], 3, axis=2) horizontal=horizontal.astype(np.uint8) imgray = cv2.cvtColor(horizontal, cv2.COLOR_BGR2GRAY) @@ -1582,8 +1519,6 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, matrix_of_lines_ch[len(cy_main_hor):,9]=1 - - if contours_h is not None: slope_lines_head,dist_x_head, x_min_main_head ,x_max_main_head ,cy_main_head,slope_lines_org_head,y_min_main_head, y_max_main_head, cx_main_head=find_features_of_lines(contours_h) matrix_l_n=np.zeros((matrix_of_lines_ch.shape[0]+len(cy_main_head),matrix_of_lines_ch.shape[1])) @@ -1629,8 +1564,6 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, args_big_parts=np.array(range(len(splitter_y_new_diff))) [ splitter_y_new_diff>22 ] - - regions_without_separators=return_regions_without_separators(region_pre_p) @@ -1640,19 +1573,8 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, peaks_neg_fin_fin=[] for itiles in args_big_parts: - - regions_without_separators_tile=regions_without_separators[int(splitter_y_new[itiles]):int(splitter_y_new[itiles+1]),:,0] - #image_page_background_zero_tile=image_page_background_zero[int(splitter_y_new[itiles]):int(splitter_y_new[itiles+1]),:] - - #print(regions_without_separators_tile.shape) - ##plt.imshow(regions_without_separators_tile) - ##plt.show() - - #num_col, peaks_neg_fin=self.find_num_col(regions_without_separators_tile,multiplier=6.0) - - #regions_without_separators_tile=cv2.erode(regions_without_separators_tile,kernel,iterations = 3) - # + try: num_col, peaks_neg_fin = find_num_col(regions_without_separators_tile, num_col_classifier, tables, multiplier=7.0) except: @@ -1670,9 +1592,6 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, peaks_neg_fin=peaks_neg_fin[peaks_neg_fin<=(vertical.shape[1]-500)] peaks_neg_fin_fin=peaks_neg_fin[:] - #print(peaks_neg_fin_fin,'peaks_neg_fin_fintaza') - - return num_col_fin, peaks_neg_fin_fin,matrix_of_lines_ch,splitter_y_new,separators_closeup_n diff --git a/qurator/eynollah/utils/contour.py b/qurator/eynollah/utils/contour.py index 53b39b5..8a92ace 100644 --- a/qurator/eynollah/utils/contour.py +++ b/qurator/eynollah/utils/contour.py @@ -263,7 +263,7 @@ def get_textregion_contours_in_org_image(cnts, img, slope_first): return cnts_org -def get_textregion_contours_in_org_image_light(cnts, img, slope_first): +def get_textregion_contours_in_org_image_light_old(cnts, img, slope_first): h_o = img.shape[0] w_o = img.shape[1] @@ -278,14 +278,7 @@ def get_textregion_contours_in_org_image_light(cnts, img, slope_first): img_copy = np.zeros(img.shape) img_copy = cv2.fillPoly(img_copy, pts=[cnts[i]], color=(1, 1, 1)) - # plt.imshow(img_copy) - # plt.show() - - # print(img.shape,'img') img_copy = rotation_image_new(img_copy, -slope_first) - ##print(img_copy.shape,'img_copy') - # plt.imshow(img_copy) - # plt.show() img_copy = img_copy.astype(np.uint8) imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) @@ -300,6 +293,70 @@ def get_textregion_contours_in_org_image_light(cnts, img, slope_first): return cnts_org +def return_list_of_contours_with_desired_order(ls_cons, sorted_indexes): + return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] +def do_back_rotation_and_get_cnt_back(queue_of_all_params, contours_par_per_process,indexes_r_con_per_pro, img, slope_first): + contours_textregion_per_each_subprocess = [] + index_by_text_region_contours = [] + for mv in range(len(contours_par_per_process)): + img_copy = np.zeros(img.shape) + img_copy = cv2.fillPoly(img_copy, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) + + img_copy = rotation_image_new(img_copy, -slope_first) + + img_copy = img_copy.astype(np.uint8) + imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + + cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) + cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) + # print(np.shape(cont_int[0])) + contours_textregion_per_each_subprocess.append(cont_int[0]*6) + index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) + + queue_of_all_params.put([contours_textregion_per_each_subprocess, index_by_text_region_contours]) + +def get_textregion_contours_in_org_image_light(cnts, img, slope_first): + num_cores = cpu_count() + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(cnts), num_cores + 1) + indexes_by_text_con = np.array(range(len(cnts))) + + h_o = img.shape[0] + w_o = img.shape[1] + + img = cv2.resize(img, (int(img.shape[1]/6.), int(img.shape[0]/6.)), interpolation=cv2.INTER_NEAREST) + ##cnts = list( (np.array(cnts)/2).astype(np.int16) ) + #cnts = cnts/2 + cnts = [(i/ 6).astype(np.int32) for i in cnts] + + for i in range(num_cores): + contours_par_per_process = cnts[int(nh[i]) : int(nh[i + 1])] + indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_back_rotation_and_get_cnt_back, args=(queue_of_all_params, contours_par_per_process, indexes_text_con_per_process, img, slope_first))) + + for i in range(num_cores): + processes[i].start() + + cnts_org = [] + all_index_text_con = [] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + contours_for_subprocess = list_all_par[0] + indexes_for_subprocess = list_all_par[1] + for j in range(len(contours_for_subprocess)): + cnts_org.append(contours_for_subprocess[j]) + all_index_text_con.append(indexes_for_subprocess[j]) + for i in range(num_cores): + processes[i].join() + + cnts_org = return_list_of_contours_with_desired_order(cnts_org, all_index_text_con) + + return cnts_org + def return_contours_of_interested_textline(region_pre_p, pixel): # pixels of images are identified by 5 diff --git a/qurator/eynollah/utils/separate_lines.py b/qurator/eynollah/utils/separate_lines.py index 1004a92..f8df33f 100644 --- a/qurator/eynollah/utils/separate_lines.py +++ b/qurator/eynollah/utils/separate_lines.py @@ -3,7 +3,8 @@ import cv2 from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d import os - +from multiprocessing import Process, Queue, cpu_count +from multiprocessing import Pool from .rotate import rotate_image from .contour import ( return_parent_contours, @@ -1569,8 +1570,21 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): # plt.show() return img_patch_ineterst_revised -def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=False, plotter=None): +def do_image_rotation(queue_of_all_params,angels_per_process, img_resized, sigma_des): + angels_per_each_subprocess = [] + for mv in range(len(angels_per_process)): + img_rot=rotate_image(img_resized,angels_per_process[mv]) + img_rot[img_rot!=0]=1 + try: + var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + except: + var_spectrum=0 + angels_per_each_subprocess.append(var_spectrum) + + queue_of_all_params.put([angels_per_each_subprocess]) +def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=False, plotter=None): + num_cores = cpu_count() if main_page and plotter: plotter.save_plot_of_textline_density(img_patch_org) @@ -1603,22 +1617,44 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals #plt.imshow(img_resized) #plt.show() angels=np.array([-45, 0 , 45 , 90 , ])#np.linspace(-12,12,100)#np.array([0 , 45 , 90 , -45]) - + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() - for rot in angels: - img_rot=rotate_image(img_resized,rot) - #plt.imshow(img_rot) - #plt.show() - img_rot[img_rot!=0]=1 - #neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - #print(var_spectrum,'var_spectrum') - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ##print(rot,var_spectrum,'var_spectrum') - except: - var_spectrum=0 - var_res.append(var_spectrum) + ###for rot in angels: + ###img_rot=rotate_image(img_resized,rot) + ####plt.imshow(img_rot) + ####plt.show() + ###img_rot[img_rot!=0]=1 + ####neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) + ####print(var_spectrum,'var_spectrum') + ###try: + ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + #####print(rot,var_spectrum,'var_spectrum') + ###except: + ###var_spectrum=0 + ###var_res.append(var_spectrum) + + + try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] @@ -1628,17 +1664,38 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals angels=np.linspace(ang_int-22.5,ang_int+22.5,n_tot_angles) + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] - for rot in angels: - img_rot=rotate_image(img_resized,rot) - ##plt.imshow(img_rot) - ##plt.show() - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 - var_res.append(var_spectrum) + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() + + ##var_res=[] + ##for rot in angels: + ##img_rot=rotate_image(img_resized,rot) + ####plt.imshow(img_rot) + ####plt.show() + ##img_rot[img_rot!=0]=1 + ##try: + ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ##except: + ##var_spectrum=0 + ##var_res.append(var_spectrum) try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] @@ -1650,24 +1707,46 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals #plt.imshow(img_resized) #plt.show() angels=np.linspace(-12,12,n_tot_angles)#np.array([0 , 45 , 90 , -45]) - - + + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() - for rot in angels: - img_rot=rotate_image(img_resized,rot) - #plt.imshow(img_rot) - #plt.show() - img_rot[img_rot!=0]=1 - #neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - #print(var_spectrum,'var_spectrum') - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 + ##var_res=[] - var_res.append(var_spectrum) + ##for rot in angels: + ##img_rot=rotate_image(img_resized,rot) + ###plt.imshow(img_rot) + ###plt.show() + ##img_rot[img_rot!=0]=1 + ###neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) + ###print(var_spectrum,'var_spectrum') + ##try: + ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + + ##except: + ##var_spectrum=0 + + ##var_res.append(var_spectrum) if plotter: @@ -1681,17 +1760,38 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals early_slope_edge=11 if abs(ang_int)>early_slope_edge and ang_int<0: angels=np.linspace(-90,-12,n_tot_angles) + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] - for rot in angels: - img_rot=rotate_image(img_resized,rot) - ##plt.imshow(img_rot) - ##plt.show() - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 - var_res.append(var_spectrum) + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() + ##var_res=[] + ##for rot in angels: + ##img_rot=rotate_image(img_resized,rot) + ####plt.imshow(img_rot) + ####plt.show() + ##img_rot[img_rot!=0]=1 + ##try: + ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ##except: + ##var_spectrum=0 + ##var_res.append(var_spectrum) try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] @@ -1701,18 +1801,41 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals elif abs(ang_int)>early_slope_edge and ang_int>0: angels=np.linspace(90,12,n_tot_angles) + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] - for rot in angels: - img_rot=rotate_image(img_resized,rot) - ##plt.imshow(img_rot) - ##plt.show() - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - #print(indexer,'indexer') - except: - var_spectrum=0 - var_res.append(var_spectrum) + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() + + + ###var_res=[] + ###for rot in angels: + ###img_rot=rotate_image(img_resized,rot) + #####plt.imshow(img_rot) + #####plt.show() + ###img_rot[img_rot!=0]=1 + ###try: + ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ####print(indexer,'indexer') + ###except: + ###var_spectrum=0 + ###var_res.append(var_spectrum) try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] @@ -1720,20 +1843,42 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals ang_int=0 else: angels=np.linspace(-25,25,int(n_tot_angles/2.)+10) - var_res=[] indexer=0 - for rot in angels: - img_rot=rotate_image(img_resized,rot) - #plt.imshow(img_rot) - #plt.show() - img_rot[img_rot!=0]=1 - #neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - #print(var_spectrum,'var_spectrum') - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 - var_res.append(var_spectrum) + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + + var_res=[] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() + ####var_res=[] + + ####for rot in angels: + ####img_rot=rotate_image(img_resized,rot) + #####plt.imshow(img_rot) + #####plt.show() + ####img_rot[img_rot!=0]=1 + #####neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) + #####print(var_spectrum,'var_spectrum') + ####try: + ####var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ####except: + ####var_spectrum=0 + ####var_res.append(var_spectrum) try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] @@ -1750,19 +1895,40 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals if abs(ang_int)>early_slope_edge and ang_int<0: angels=np.linspace(-90,-25,int(n_tot_angles/2.)+10) - + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + var_res=[] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() - for rot in angels: - img_rot=rotate_image(img_resized,rot) - ##plt.imshow(img_rot) - ##plt.show() - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 - var_res.append(var_spectrum) + ###var_res=[] + + ###for rot in angels: + ###img_rot=rotate_image(img_resized,rot) + #####plt.imshow(img_rot) + #####plt.show() + ###img_rot[img_rot!=0]=1 + ###try: + ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ###except: + ###var_spectrum=0 + ###var_res.append(var_spectrum) try: var_res=np.array(var_res) @@ -1773,22 +1939,44 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals elif abs(ang_int)>early_slope_edge and ang_int>0: angels=np.linspace(90,25,int(n_tot_angles/2.)+10) - - var_res=[] - indexer=0 - for rot in angels: - img_rot=rotate_image(img_resized,rot) - ##plt.imshow(img_rot) - ##plt.show() - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - #print(indexer,'indexer') - except: - var_spectrum=0 + + queue_of_all_params = Queue() + processes = [] + nh = np.linspace(0, len(angels), num_cores + 1) + + for i in range(num_cores): + angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] + processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) + + for i in range(num_cores): + processes[i].start() + + var_res=[] + for i in range(num_cores): + list_all_par = queue_of_all_params.get(True) + angles_for_subprocess = list_all_par[0] + for j in range(len(angles_for_subprocess)): + var_res.append(angles_for_subprocess[j]) + + for i in range(num_cores): + processes[i].join() - var_res.append(var_spectrum) + ###var_res=[] + + + ###for rot in angels: + ###img_rot=rotate_image(img_resized,rot) + #####plt.imshow(img_rot) + #####plt.show() + ###img_rot[img_rot!=0]=1 + ###try: + ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) + ####print(indexer,'indexer') + ###except: + ###var_spectrum=0 + + ###var_res.append(var_spectrum) try: var_res=np.array(var_res) ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] From 70772d41042df2415a0918d99f51cb183db36fe5 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 21 Oct 2024 23:46:38 +0200 Subject: [PATCH 261/412] binarization as a standalone command --- qurator/eynollah/cli.py | 33 +++ qurator/eynollah/eynollah.py | 5 +- qurator/eynollah/sbb_binarize.py | 383 +++++++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 qurator/eynollah/sbb_binarize.py diff --git a/qurator/eynollah/cli.py b/qurator/eynollah/cli.py index 4c762a8..0daf0c9 100644 --- a/qurator/eynollah/cli.py +++ b/qurator/eynollah/cli.py @@ -2,6 +2,7 @@ import sys import click from ocrd_utils import initLogging, setOverrideLogLevel from qurator.eynollah.eynollah import Eynollah +from qurator.eynollah.sbb_binarize import SbbBinarizer @click.group() def main(): @@ -48,6 +49,38 @@ def main(): def machine_based_reading_order(dir_xml, dir_out_modal_image, dir_out_classes, input_height, input_width, min_area_size): xml_files_ind = os.listdir(dir_xml) +@main.command() +@click.option('--patches/--no-patches', default=True, help='by enabling this parameter you let the model to see the image in patches.') + +@click.option('--model_dir', '-m', type=click.Path(exists=True, file_okay=False), required=True, help='directory containing models for prediction') + +@click.argument('input_image') + +@click.argument('output_image') +@click.option( + "--dir_in", + "-di", + help="directory of images", + type=click.Path(exists=True, file_okay=False), +) +@click.option( + "--dir_out", + "-do", + help="directory where the binarized images will be written", + type=click.Path(exists=True, file_okay=False), +) + +def binarization(patches, model_dir, input_image, output_image, dir_in, dir_out): + if not dir_out and (dir_in): + print("Error: You used -di but did not set -do") + sys.exit(1) + elif dir_out and not (dir_in): + print("Error: You used -do to write out binarized images but have not set -di") + sys.exit(1) + SbbBinarizer(model_dir).run(image_path=input_image, use_patches=patches, save=output_image, dir_in=dir_in, dir_out=dir_out) + + + @main.command() @click.option( diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 79724cc..e587ff3 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -240,7 +240,6 @@ class Eynollah: pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') self.dir_models = dir_models - self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425" self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425" self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" @@ -4769,9 +4768,9 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea) #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ diff --git a/qurator/eynollah/sbb_binarize.py b/qurator/eynollah/sbb_binarize.py new file mode 100644 index 0000000..36e9ab0 --- /dev/null +++ b/qurator/eynollah/sbb_binarize.py @@ -0,0 +1,383 @@ +""" +Tool to load model and binarize a given image. +""" + +import sys +from glob import glob +from os import environ, devnull +from os.path import join +from warnings import catch_warnings, simplefilter +import os + +import numpy as np +from PIL import Image +import cv2 +environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +stderr = sys.stderr +sys.stderr = open(devnull, 'w') +import tensorflow as tf +from tensorflow.keras.models import load_model +from tensorflow.python.keras import backend as tensorflow_backend +sys.stderr = stderr + + +import logging + +def resize_image(img_in, input_height, input_width): + return cv2.resize(img_in, (input_width, input_height), interpolation=cv2.INTER_NEAREST) + +class SbbBinarizer: + + def __init__(self, model_dir, logger=None): + self.model_dir = model_dir + self.log = logger if logger else logging.getLogger('SbbBinarizer') + + self.start_new_session() + + self.model_files = glob(self.model_dir+"/*/", recursive = True) + + self.models = [] + for model_file in self.model_files: + self.models.append(self.load_model(model_file)) + + def start_new_session(self): + config = tf.compat.v1.ConfigProto() + config.gpu_options.allow_growth = True + + self.session = tf.compat.v1.Session(config=config) # tf.InteractiveSession() + tensorflow_backend.set_session(self.session) + + def end_session(self): + tensorflow_backend.clear_session() + self.session.close() + del self.session + + def load_model(self, model_name): + model = load_model(join(self.model_dir, model_name), compile=False) + model_height = model.layers[len(model.layers)-1].output_shape[1] + model_width = model.layers[len(model.layers)-1].output_shape[2] + n_classes = model.layers[len(model.layers)-1].output_shape[3] + return model, model_height, model_width, n_classes + + def predict(self, model_in, img, use_patches, n_batch_inference=5): + tensorflow_backend.set_session(self.session) + model, model_height, model_width, n_classes = model_in + + img_org_h = img.shape[0] + img_org_w = img.shape[1] + + if img.shape[0] < model_height and img.shape[1] >= model_width: + img_padded = np.zeros(( model_height, img.shape[1], img.shape[2] )) + + index_start_h = int( abs( img.shape[0] - model_height) /2.) + index_start_w = 0 + + img_padded [ index_start_h: index_start_h+img.shape[0], :, : ] = img[:,:,:] + + elif img.shape[0] >= model_height and img.shape[1] < model_width: + img_padded = np.zeros(( img.shape[0], model_width, img.shape[2] )) + + index_start_h = 0 + index_start_w = int( abs( img.shape[1] - model_width) /2.) + + img_padded [ :, index_start_w: index_start_w+img.shape[1], : ] = img[:,:,:] + + + elif img.shape[0] < model_height and img.shape[1] < model_width: + img_padded = np.zeros(( model_height, model_width, img.shape[2] )) + + index_start_h = int( abs( img.shape[0] - model_height) /2.) + index_start_w = int( abs( img.shape[1] - model_width) /2.) + + img_padded [ index_start_h: index_start_h+img.shape[0], index_start_w: index_start_w+img.shape[1], : ] = img[:,:,:] + + else: + index_start_h = 0 + index_start_w = 0 + img_padded = np.copy(img) + + + img = np.copy(img_padded) + + + + if use_patches: + + margin = int(0.1 * model_width) + + width_mid = model_width - 2 * margin + height_mid = model_height - 2 * margin + + + img = img / float(255.0) + + img_h = img.shape[0] + img_w = img.shape[1] + + prediction_true = np.zeros((img_h, img_w, 3)) + mask_true = np.zeros((img_h, img_w)) + nxf = img_w / float(width_mid) + nyf = img_h / float(height_mid) + + if nxf > int(nxf): + nxf = int(nxf) + 1 + else: + nxf = int(nxf) + + if nyf > int(nyf): + nyf = int(nyf) + 1 + else: + nyf = int(nyf) + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, model_height, model_width,3)) + + for i in range(nxf): + for j in range(nyf): + + if i == 0: + index_x_d = i * width_mid + index_x_u = index_x_d + model_width + elif i > 0: + index_x_d = i * width_mid + index_x_u = index_x_d + model_width + + if j == 0: + index_y_d = j * height_mid + index_y_u = index_y_d + model_height + elif j > 0: + index_y_d = j * height_mid + index_y_u = index_y_d + model_height + + if index_x_u > img_w: + index_x_u = img_w + index_x_d = img_w - model_width + if index_y_u > img_h: + index_y_u = img_h + index_y_d = img_h - model_height + + + list_i_s.append(i) + list_j_s.append(j) + list_x_u.append(index_x_u) + list_x_d.append(index_x_d) + list_y_d.append(index_y_d) + list_y_u.append(index_y_u) + + + img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] + + batch_indexer = batch_indexer + 1 + + + + if batch_indexer == n_batch_inference: + + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + + #print(seg.shape, len(seg), len(list_i_s)) + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, model_height, model_width,3)) + + elif i==(nxf-1) and j==(nyf-1): + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + + #print(seg.shape, len(seg), len(list_i_s)) + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, model_height, model_width,3)) + + + + prediction_true = prediction_true[index_start_h: index_start_h+img_org_h, index_start_w: index_start_w+img_org_w,:] + prediction_true = prediction_true.astype(np.uint8) + + else: + img_h_page = img.shape[0] + img_w_page = img.shape[1] + img = img / float(255.0) + img = resize_image(img, model_height, model_width) + + label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2])) + + seg = np.argmax(label_p_pred, axis=3)[0] + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + prediction_true = resize_image(seg_color, img_h_page, img_w_page) + prediction_true = prediction_true.astype(np.uint8) + return prediction_true[:,:,0] + + def run(self, image=None, image_path=None, save=None, use_patches=False, dir_in=None, dir_out=None): + print(dir_in,'dir_in') + if not dir_in: + if (image is not None and image_path is not None) or \ + (image is None and image_path is None): + raise ValueError("Must pass either a opencv2 image or an image_path") + if image_path is not None: + image = cv2.imread(image_path) + img_last = 0 + for n, (model, model_file) in enumerate(zip(self.models, self.model_files)): + self.log.info('Predicting with model %s [%s/%s]' % (model_file, n + 1, len(self.model_files))) + + res = self.predict(model, image, use_patches) + + img_fin = np.zeros((res.shape[0], res.shape[1], 3)) + res[:, :][res[:, :] == 0] = 2 + res = res - 1 + res = res * 255 + img_fin[:, :, 0] = res + img_fin[:, :, 1] = res + img_fin[:, :, 2] = res + + img_fin = img_fin.astype(np.uint8) + img_fin = (res[:, :] == 0) * 255 + img_last = img_last + img_fin + + kernel = np.ones((5, 5), np.uint8) + img_last[:, :][img_last[:, :] > 0] = 255 + img_last = (img_last[:, :] == 0) * 255 + if save: + cv2.imwrite(save, img_last) + return img_last + else: + ls_imgs = os.listdir(dir_in) + for image_name in ls_imgs: + image_stem = image_name.split('.')[0] + print(image_name,'image_name') + image = cv2.imread(os.path.join(dir_in,image_name) ) + img_last = 0 + for n, (model, model_file) in enumerate(zip(self.models, self.model_files)): + self.log.info('Predicting with model %s [%s/%s]' % (model_file, n + 1, len(self.model_files))) + + res = self.predict(model, image, use_patches) + + img_fin = np.zeros((res.shape[0], res.shape[1], 3)) + res[:, :][res[:, :] == 0] = 2 + res = res - 1 + res = res * 255 + img_fin[:, :, 0] = res + img_fin[:, :, 1] = res + img_fin[:, :, 2] = res + + img_fin = img_fin.astype(np.uint8) + img_fin = (res[:, :] == 0) * 255 + img_last = img_last + img_fin + + kernel = np.ones((5, 5), np.uint8) + img_last[:, :][img_last[:, :] > 0] = 255 + img_last = (img_last[:, :] == 0) * 255 + + cv2.imwrite(os.path.join(dir_out,image_stem+'.png'), img_last) From 328d33e3dc294b4d93fcdca833ed679ee0169f9f Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 23 Oct 2024 16:55:41 +0200 Subject: [PATCH 262/412] =?UTF-8?q?Temporary=20commit=20=E2=80=93=20textli?= =?UTF-8?q?ne=20prediction=20without=20patches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qurator/eynollah/eynollah.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index e587ff3..6ee3dc7 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2120,7 +2120,7 @@ class Eynollah: else: thresholding_for_artificial_class_in_light_version = False if not self.dir_in: - model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir if patches else self.model_textline_dir_np) + model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir) #img = img.astype(np.uint8) img_org = np.copy(img) img_h = img_org.shape[0] @@ -3311,7 +3311,8 @@ class Eynollah: scaler_h_textline = 1#1.3 # 1.2#1.2 scaler_w_textline = 1#1.3 # 0.9#1 #print(image_page.shape) - textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline, num_col_classifier) + patches = False + textline_mask_tot_ea, _ = self.textline_contours(image_page, patches, scaler_h_textline, scaler_w_textline, num_col_classifier) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) From 82281bd6cfa218e7e434fe8da535fae394d5f59c Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 25 Oct 2024 19:42:48 +0200 Subject: [PATCH 263/412] fixing a bug occuring with reading order + Slro option with no patch textline model and thresholding artificial class --- qurator/eynollah/eynollah.py | 71 ++++++++++++++++++------------ qurator/eynollah/utils/__init__.py | 21 ++++----- qurator/eynollah/utils/xml.py | 2 +- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index e587ff3..03252fb 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -245,7 +245,7 @@ class Eynollah: self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" - self.model_region_dir_fully_np = dir_models + "/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully_np = dir_models + "/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" @@ -253,11 +253,11 @@ class Eynollah: self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" - self.model_region_dir_fully = dir_models + "/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: - self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# + self.model_textline_dir = dir_models + "/model_textline_ens_5_6_7_8_10_11_nopatch"#"/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: - self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" + self.model_textline_dir = dir_models + "/model_textline_ens_5_6_7_8_10_11_nopatch"#"/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" if self.ocr: self.model_ocr_dir = dir_models + "/checkpoint-166692_printed_trocr" @@ -816,6 +816,14 @@ class Eynollah: verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] + + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[0,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) prediction_true = prediction_true.astype(np.uint8) @@ -1546,7 +1554,7 @@ class Eynollah: pass else: img = otsu_copy_binary(img) - img = img.astype(np.uint8) + #img = img.astype(np.uint8) prediction_regions2 = None else: if cols == 1: @@ -1605,9 +1613,12 @@ class Eynollah: img = img.astype(np.uint8) marginal_of_patch_percent = 0.1 - + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=3) + + ##prediction_regions = self.do_prediction(False, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=3) + prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions @@ -2148,7 +2159,7 @@ class Eynollah: if not thresholding_for_artificial_class_in_light_version: textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') - textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) + #textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) prediction_textline[:,:][textline_mask_tot_ea_art[:,:]==1]=2 @@ -2245,26 +2256,27 @@ class Eynollah: #if (not self.input_binary) or self.full_layout: #if self.input_binary: #img_bin = np.copy(img_resized) - if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 30): - if not self.dir_in: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) - else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + ###if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 30): + ###if not self.dir_in: + ###model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + ###prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) + ###else: + ###prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) - #print("inside bin ", time.time()-t_bin) - prediction_bin=prediction_bin[:,:,0] - prediction_bin = (prediction_bin[:,:]==0)*1 - prediction_bin = prediction_bin*255 + ####print("inside bin ", time.time()-t_bin) + ###prediction_bin=prediction_bin[:,:,0] + ###prediction_bin = (prediction_bin[:,:]==0)*1 + ###prediction_bin = prediction_bin*255 - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) + ###prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - prediction_bin = prediction_bin.astype(np.uint16) - #img= np.copy(prediction_bin) - img_bin = np.copy(prediction_bin) - else: - img_bin = np.copy(img_resized) + ###prediction_bin = prediction_bin.astype(np.uint16) + ####img= np.copy(prediction_bin) + ###img_bin = np.copy(prediction_bin) + ###else: + ###img_bin = np.copy(img_resized) + img_bin = np.copy(img_resized) #print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) @@ -3311,7 +3323,8 @@ class Eynollah: scaler_h_textline = 1#1.3 # 1.2#1.2 scaler_w_textline = 1#1.3 # 0.9#1 #print(image_page.shape) - textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline, num_col_classifier) + patches = False + textline_mask_tot_ea, _ = self.textline_contours(image_page, patches, scaler_h_textline, scaler_w_textline, num_col_classifier) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) @@ -3564,9 +3577,9 @@ class Eynollah: image_page = image_page.astype(np.uint8) #print("full inside 1", time.time()- t_full0) if self.light_version: - regions_fully, regions_fully_only_drop = self.extract_text_regions_new(img_bin_light, True, cols=num_col_classifier) + regions_fully, regions_fully_only_drop = self.extract_text_regions_new(img_bin_light, False, cols=num_col_classifier) else: - regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, True, cols=num_col_classifier) + regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, False, cols=num_col_classifier) #print("full inside 2", time.time()- t_full0) # 6 is the separators lable in old full layout model # 4 is the drop capital class in old full layout model @@ -3590,7 +3603,7 @@ class Eynollah: regions_fully[:,:,0][drops[:,:]==1] = drop_capital_label_in_full_layout_model - regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) + ##regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 @@ -4768,9 +4781,9 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = 0, 0#self.run_deskew(textline_mask_tot_ea) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index 6219df2..e7cbbea 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -1204,17 +1204,12 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): top = peaks_neg_new[i] down = peaks_neg_new[i + 1] - # print(top,down,'topdown') - indexes_in = matrix_of_orders[:, 0][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] cxs_in = matrix_of_orders[:, 2][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] cys_in = matrix_of_orders[:, 3][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] types_of_text = matrix_of_orders[:, 1][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] index_types_of_text = matrix_of_orders[:, 4][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - # print(top,down) - # print(cys_in,'cyyyins') - # print(indexes_in,'indexes') sorted_inside = np.argsort(cxs_in) ind_in_int = indexes_in[sorted_inside] @@ -1228,11 +1223,17 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): ##matrix_of_orders[:len_main,4]=final_indexers_sorted[:] - # print(peaks_neg_new,'peaks') - # print(final_indexers_sorted,'indexsorted') - # print(final_types,'types') - # print(final_index_type,'final_index_type') - + # This fix is applied if the sum of the lengths of contours and contours_h does not match final_indexers_sorted. However, this is not the optimal solution.. + if (len(cy_main)+len(cy_header) ) == len(final_index_type): + pass + else: + indexes_missed = set(list( np.array( range((len(cy_main)+len(cy_header) ) )) )) - set(final_indexers_sorted) + for ind_missed in indexes_missed: + final_indexers_sorted.append(ind_missed) + final_types.append(1) + final_index_type.append(ind_missed) + + return final_indexers_sorted, matrix_of_orders, final_types, final_index_type def combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(img_p_in_ver, img_in_hor,num_col_classifier): diff --git a/qurator/eynollah/utils/xml.py b/qurator/eynollah/utils/xml.py index 0386b25..bd95702 100644 --- a/qurator/eynollah/utils/xml.py +++ b/qurator/eynollah/utils/xml.py @@ -72,7 +72,7 @@ def order_and_id_of_texts(found_polygons_text_region, found_polygons_text_region index_of_types_2 = index_of_types[kind_of_texts == 2] indexes_sorted_2 = indexes_sorted[kind_of_texts == 2] - + counter = EynollahIdCounter(region_idx=ref_point) for idx_textregion, _ in enumerate(found_polygons_text_region): id_of_texts.append(counter.next_region_id) From 90ee2d61dc1d2ce05724d6d0f11c200ba1709108 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 28 Oct 2024 20:56:06 +0100 Subject: [PATCH 264/412] textline segmentation is masked with drop capitals --- qurator/eynollah/eynollah.py | 223 +++++++++++++++++++++-------------- 1 file changed, 135 insertions(+), 88 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 1cb00c7..d0a8299 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -245,7 +245,7 @@ class Eynollah: self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" - self.model_region_dir_fully_np = dir_models + "/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" @@ -253,11 +253,11 @@ class Eynollah: self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" - self.model_region_dir_fully = dir_models + "/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: - self.model_textline_dir = dir_models + "/model_textline_ens_5_6_7_8_10_11_nopatch"#"/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: - self.model_textline_dir = dir_models + "/model_textline_ens_5_6_7_8_10_11_nopatch"#"/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" if self.ocr: self.model_ocr_dir = dir_models + "/checkpoint-166692_printed_trocr" @@ -502,7 +502,8 @@ class Eynollah: if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) num_column_is_classified = False - elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + #elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + elif img_h_new >= 8000: img_new = np.copy(img) num_column_is_classified = False else: @@ -523,7 +524,8 @@ class Eynollah: if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) num_column_is_classified = False - elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + #elif label_p_pred[0][int(num_col - 1)] < 0.8 and img_h_new >= 8000: + elif img_h_new >= 8000: img_new = np.copy(img) num_column_is_classified = False else: @@ -3323,7 +3325,7 @@ class Eynollah: scaler_h_textline = 1#1.3 # 1.2#1.2 scaler_w_textline = 1#1.3 # 0.9#1 #print(image_page.shape) - patches = False + patches = True textline_mask_tot_ea, _ = self.textline_contours(image_page, patches, scaler_h_textline, scaler_w_textline, num_col_classifier) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) @@ -3634,6 +3636,7 @@ class Eynollah: regions_without_separators = (text_regions_p[:, :] == 1) * 1 img_revised_tab = np.copy(text_regions_p[:, :]) polygons_of_images = return_contours_of_interested_region(img_revised_tab, 5) + self.logger.debug('exit run_boxes_full_layout') #print("full inside 3", time.time()- t_full0) return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables @@ -4169,7 +4172,123 @@ class Eynollah: x_differential_new[split_masked[i]:split_masked[i+1]] = -1*np.array(x_differential)[split_masked[i]:split_masked[i+1]] return x_differential_new - + def dilate_textregions_contours_textline_version(self,all_found_textline_polygons): + #print(all_found_textline_polygons) + + for j in range(len(all_found_textline_polygons)): + for ij in range(len(all_found_textline_polygons[j])): + + con_ind = all_found_textline_polygons[j][ij] + area = cv2.contourArea(con_ind) + con_ind = con_ind.astype(np.float) + + x_differential = np.diff( con_ind[:,0,0]) + y_differential = np.diff( con_ind[:,0,1]) + + + x_differential = gaussian_filter1d(x_differential, 0.1) + y_differential = gaussian_filter1d(y_differential, 0.1) + + x_min = float(np.min( con_ind[:,0,0] )) + y_min = float(np.min( con_ind[:,0,1] )) + + x_max = float(np.max( con_ind[:,0,0] )) + y_max = float(np.max( con_ind[:,0,1] )) + + x_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in x_differential] + y_differential_mask_nonzeros = [ ind/abs(ind) if ind!=0 else ind for ind in y_differential] + + abs_diff=abs(abs(x_differential)- abs(y_differential) ) + + inc_x = np.zeros(len(x_differential)+1) + inc_y = np.zeros(len(x_differential)+1) + + + if (y_max-y_min) <= (x_max-x_min): + dilation_m1 = round(area / (x_max-x_min) * 0.12) + else: + dilation_m1 = round(area / (y_max-y_min) * 0.12) + + if dilation_m1>8: + dilation_m1 = 8 + if dilation_m1<6: + dilation_m1 = 6 + #print(dilation_m1, 'dilation_m1') + dilation_m1 = 6 + dilation_m2 = int(dilation_m1/2.) +1 + + for i in range(len(x_differential)): + if abs_diff[i]==0: + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) + elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) + + elif abs_diff[i]!=0 and abs_diff[i]>=3: + if abs(x_differential[i])>abs(y_differential[i]): + inc_y[i+1] = dilation_m1*(x_differential_mask_nonzeros[i]) + else: + inc_x[i+1]= dilation_m1*(-1*y_differential_mask_nonzeros[i]) + else: + inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) + inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) + + + inc_x[0] = inc_x[-1] + inc_y[0] = inc_y[-1] + + con_scaled = con_ind*1 + + con_scaled[:,0, 0] = con_ind[:,0,0] + np.array(inc_x)[:] + con_scaled[:,0, 1] = con_ind[:,0,1] + np.array(inc_y)[:] + + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 + con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 + + area_scaled = cv2.contourArea(con_scaled.astype(np.int32)) + + con_ind = con_ind.astype(np.int32) + + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] + + results = np.array(results) + + #print(results,'results') + + results[results==0] = 1 + + + diff_result = np.diff(results) + + indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] + indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] + + + if results[0]==1: + con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] + con_scaled[:indices_m2[0]+1,0, 0] = con_ind[:indices_m2[0]+1,0,0] + #indices_2 = indices_2[1:] + indices_m2 = indices_m2[1:] + + + + if len(indices_2)>len(indices_m2): + con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] + con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] + + indices_2 = indices_2[:-1] + + + for ii in range(len(indices_2)): + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] + con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] + + + all_found_textline_polygons[j][ij][:,0,1] = con_scaled[:,0, 1] + all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] + return all_found_textline_polygons def dilate_textregions_contours(self,all_found_textline_polygons): #print(all_found_textline_polygons) for j in range(len(all_found_textline_polygons)): @@ -4179,9 +4298,6 @@ class Eynollah: area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) - #con_ind[:,0,0] = gaussian_filter1d(con_ind[:,0,0], 0.5) - #con_ind[:,0,1] = gaussian_filter1d(con_ind[:,0,1], 0.5) - x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) @@ -4235,29 +4351,6 @@ class Eynollah: inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) - ###for i in range(len(x_differential)): - ###if abs_diff[i]==0: - ###inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) - ###inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) - ###elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]==0 and y_differential_mask_nonzeros[i]!=0: - ###inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) - ###elif abs_diff[i]!=0 and x_differential_mask_nonzeros[i]!=0 and y_differential_mask_nonzeros[i]==0: - ###inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) - - ###elif abs_diff[i]!=0 and abs_diff[i]>=3: - ###if abs(x_differential[i])>abs(y_differential[i]): - ###inc_y[i+1] = 12*(x_differential_mask_nonzeros[i]) - ###else: - ###inc_x[i+1]= 12*(-1*y_differential_mask_nonzeros[i]) - ###else: - ###inc_x[i+1] = 7*(-1*y_differential_mask_nonzeros[i]) - ###inc_y[i+1] = 7*(x_differential_mask_nonzeros[i]) - - ###inc_x =list(inc_x) - ###inc_x.append(inc_x[0]) - - ###inc_y =list(inc_y) - ###inc_y.append(inc_y[0]) inc_x[0] = inc_x[-1] inc_y[0] = inc_y[-1] @@ -4288,21 +4381,6 @@ class Eynollah: indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] - #print(area_scaled / area, "ratio") - #print(results,'results') - #if results[0]==1 and diff_result[-1]==-2: - ##indices_2 = indices_2[1:] - ##indices_m2 = indices_m2[1:] - - #con_scaled[:indices_m2[0]+1,0, 1] = con_scaled[indices_m2[-1],0, 1] - #con_scaled[:indices_m2[0]+1,0, 0] = con_scaled[indices_m2[-1],0, 0] - - - #con_scaled[indices_2[-1]+1:,0, 1] = con_scaled[indices_m2[-1],0, 1] - #con_scaled[indices_2[-1]+1:,0, 0] = con_scaled[indices_m2[-1],0, 0] - - #indices_2 = indices_2[:-1] - #indices_m2 = indices_m2[1:-1] if results[0]==1: con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] @@ -4318,50 +4396,12 @@ class Eynollah: indices_2 = indices_2[:-1] - - - #diff_neg_pos = np.array(indices_m2) - np.array(indices_2) - - - #print(diff_neg_pos,'diff') - ##print(indices_2, 'indices_2') - #indices_2 = np.array(indices_2)[diff_neg_pos>1] - #indices_m2 = np.array(indices_m2)[diff_neg_pos>1] for ii in range(len(indices_2)): - #x_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 0] - #y_inner = con_ind[indices_2[ii]+1:indices_m2[ii]+1,0, 1] - - #if x_inner[-1]>=x_inner[0]: - #x_interest = np.min(x_inner) - #else: - #x_interest = np.max(x_inner) - - #if y_inner[-1]>=y_inner[0]: - #y_interest = np.min(y_inner) - #else: - #y_interest = np.max(y_inner) - con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] - - #con_scaled[:,0, 1][results[:]>0] = con_ind[:,0,1][results[:]>0] - #con_scaled[:,0, 0][results[:]>0] = con_ind[:,0,0][results[:]>0] - - #print(list(results), 'results') - #print(list(diff_result), 'diff_result') - #print(indices_2,'2') - #print(indices_m2,'-2') - #print(diff_neg_pos,'diff_neg_pos') - - ##con_scaled[:,0, 1] = gaussian_filter1d(con_scaled[:,0, 1], 0.1) - ##con_scaled[:,0, 0] = gaussian_filter1d(con_scaled[:,0, 0], 0.1) - - #con_scaled[-1,0, 1] = con_scaled[0,0, 1] - #con_scaled[-1,0, 0] = con_scaled[0,0, 0] - ##print(len(con_scaled[:,0,0]),'con_scaled[:,0,0]') all_found_textline_polygons[j][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons @@ -4865,6 +4905,12 @@ class Eynollah: img_bin_light = None polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + + if self.light_version: + drop_label_in_full_layout = 4 + textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 + + text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 @@ -5018,7 +5064,8 @@ class Eynollah: #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") all_found_textline_polygons_marginals = self.dilate_textline_contours(all_found_textline_polygons_marginals) From 438df5228705e93f52d43a17a9284cc199fb97f4 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 30 Oct 2024 00:52:09 +0100 Subject: [PATCH 265/412] updating --- qurator/eynollah/eynollah.py | 8 +++++--- qurator/eynollah/utils/__init__.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index d0a8299..543ed92 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -1726,6 +1726,7 @@ class Eynollah: polygons_of_textlines = return_contours_of_interested_region(textline_mask_tot,1,0.00001) + M_main_tot = [cv2.moments(polygons_of_textlines[j]) for j in range(len(polygons_of_textlines))] cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] @@ -3605,7 +3606,7 @@ class Eynollah: regions_fully[:,:,0][drops[:,:]==1] = drop_capital_label_in_full_layout_model - ##regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) + regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 @@ -4901,6 +4902,7 @@ class Eynollah: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.full_layout: + cv2.imwrite('dewar_page.png', image_page) if not self.light_version: img_bin_light = None polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) @@ -5067,7 +5069,7 @@ class Eynollah: #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textline_contours(all_found_textline_polygons_marginals) + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) @@ -5261,7 +5263,7 @@ class Eynollah: all_found_textline_polygons=[ all_found_textline_polygons ] - all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") diff --git a/qurator/eynollah/utils/__init__.py b/qurator/eynollah/utils/__init__.py index e7cbbea..29f80b4 100644 --- a/qurator/eynollah/utils/__init__.py +++ b/qurator/eynollah/utils/__init__.py @@ -792,7 +792,7 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) - if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.8: + if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.4: layout_in_patch[y : y + h, x : x + w, 0] = drop_capital_label else: From e796a99c5cae651ae1601f2033feecd695b382f2 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 30 Oct 2024 15:02:50 +0100 Subject: [PATCH 266/412] updating inference for early layout in the case of documents with number of columns bigger than 2 --- qurator/eynollah/eynollah.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 543ed92..0a1c2b1 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -2296,9 +2296,8 @@ class Eynollah: #plt.show() if not skip_layout_and_reading_order: #print("inside 2 ", time.time()-t_in) - if not self.dir_in: - if num_col_classifier == 1 or num_col_classifier >= 2: + if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) @@ -2307,12 +2306,12 @@ class Eynollah: prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, model_region) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: - if num_col_classifier == 1 or num_col_classifier >= 2: + if num_col_classifier == 1 or num_col_classifier == 2: if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) else: @@ -2320,7 +2319,7 @@ class Eynollah: prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: - prediction_regions_org = self.do_prediction_new_concept(True, img_bin, self.model_region, n_batch_inference=3) + prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), self.model_region_1_2, n_batch_inference=2) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) From 751b0102f7787f2ab8a45e3ecc4604e7c107e1e6 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 5 Nov 2024 19:50:18 +0100 Subject: [PATCH 267/412] updating early layout inference for light version --- qurator/eynollah/eynollah.py | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/qurator/eynollah/eynollah.py b/qurator/eynollah/eynollah.py index 0a1c2b1..9095c15 100644 --- a/qurator/eynollah/eynollah.py +++ b/qurator/eynollah/eynollah.py @@ -245,7 +245,7 @@ class Eynollah: self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" - self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" @@ -253,7 +253,7 @@ class Eynollah: self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" - self.model_region_dir_fully = dir_models + "/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: @@ -743,7 +743,7 @@ class Eynollah: def get_image_and_scales_after_enhancing(self, img_org, img_res): self.logger.debug("enter get_image_and_scales_after_enhancing") self.image = np.copy(img_res) - #self.image = self.image.astype(np.uint8) + self.image = self.image.astype(np.uint8) self.image_org = np.copy(img_org) self.height_org = self.image_org.shape[0] self.width_org = self.image_org.shape[1] @@ -1298,20 +1298,25 @@ class Eynollah: seg = np.argmax(label_p_pred, axis=3) if thresholding_for_some_classes_in_light_version: - seg_not_base = label_p_pred[:,:,:,4] - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 + + seg_art = label_p_pred[:,:,:,4] + seg_art[seg_art<0.2] =0 + seg_art[seg_art>0] =1 + ###seg[seg_art==1]=4 + ##seg_not_base = label_p_pred[:,:,:,4] + ##seg_not_base[seg_not_base>0.03] =1 + ##seg_not_base[seg_not_base<1] =0 seg_line = label_p_pred[:,:,:,3] seg_line[seg_line>0.1] =1 seg_line[seg_line<1] =0 - seg_background = label_p_pred[:,:,:,0] - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 + ##seg_background = label_p_pred[:,:,:,0] + ##seg_background[seg_background>0.25] =1 + ##seg_background[seg_background<1] =0 - seg[seg_not_base==1]=4 - seg[seg_background==1]=0 + seg[seg_art==1]=4 + ##seg[seg_background==1]=0 seg[(seg_line==1) & (seg==0)]=3 if thresholding_for_artificial_class_in_light_version: seg_art = label_p_pred[:,:,:,2] @@ -2300,26 +2305,26 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) else: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region) + prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: if num_col_classifier == 1 or num_col_classifier == 2: if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_some_classes_in_light_version=True) else: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: - prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), self.model_region_1_2, n_batch_inference=2) + prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), self.model_region_1_2, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) @@ -4595,7 +4600,7 @@ class Eynollah: areas_without = np.array(areas_tot)[args_all] area_of_con_interest = areas_tot[ij] - args_with_bigger_area = np.array(args_all)[areas_without > area_of_con_interest] + args_with_bigger_area = np.array(args_all)[areas_without > 1.5*area_of_con_interest] if len(args_with_bigger_area)>0: results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main_tot[ij], cy_main_tot[ij]), False) for ind in args_with_bigger_area ] From 8409de0e58457f2ae4661a42ce8942e96794f2e8 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sun, 10 Nov 2024 19:34:43 +0100 Subject: [PATCH 268/412] sbb_binarization is integrated into eynollah works in framework of ocrd - sbb_binarization in ocrd works for individual images by the way as standalone flowing from directory can be used now. For eynollah in ocrd framework I have added -light version as default parameter. --- pyproject.toml | 1 + src/eynollah/eynollah.py | 1 - src/eynollah/ocrd-tool-binarization.json | 47 +++++++ src/eynollah/ocrd-tool.json | 10 ++ src/eynollah/ocrd_cli_binarization.py | 158 +++++++++++++++++++++++ src/eynollah/processor.py | 2 + 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/eynollah/ocrd-tool-binarization.json create mode 100644 src/eynollah/ocrd_cli_binarization.py diff --git a/pyproject.toml b/pyproject.toml index 67a420d..b056cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ [project.scripts] eynollah = "eynollah.cli:main" ocrd-eynollah-segment = "eynollah.ocrd_cli:main" +ocrd-sbb-binarize = "eynollah.ocrd_cli_binarization:cli" [project.urls] Homepage = "https://github.com/qurator-spk/eynollah" diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 0d0d683..29d2788 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4964,7 +4964,6 @@ class Eynollah: polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.full_layout: - cv2.imwrite('dewar_page.png', image_page) if not self.light_version: img_bin_light = None polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) diff --git a/src/eynollah/ocrd-tool-binarization.json b/src/eynollah/ocrd-tool-binarization.json new file mode 100644 index 0000000..1711e89 --- /dev/null +++ b/src/eynollah/ocrd-tool-binarization.json @@ -0,0 +1,47 @@ +{ + "version": "0.1.0", + "git_url": "https://github.com/qurator-spk/sbb_binarization", + "tools": { + "ocrd-sbb-binarize": { + "executable": "ocrd-sbb-binarize", + "description": "Pixelwise binarization with selectional auto-encoders in Keras", + "categories": ["Image preprocessing"], + "steps": ["preprocessing/optimization/binarization"], + "input_file_grp": [], + "output_file_grp": [], + "parameters": { + "operation_level": { + "type": "string", + "enum": ["page", "region"], + "default": "page", + "description": "PAGE XML hierarchy level to operate on" + }, + "model": { + "description": "Directory containing HDF5 or SavedModel/ProtoBuf models. Can be an absolute path or a path relative to the OCR-D resource location, the current working directory or the $SBB_BINARIZE_DATA environment variable (if set)", + "type": "string", + "format": "uri", + "content-type": "text/directory", + "required": true + } + }, + "resources": [ + { + "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2020_01_16.zip", + "name": "default", + "type": "archive", + "path_in_archive": "saved_model_2020_01_16", + "size": 563147331, + "description": "default models provided by github.com/qurator-spk (SavedModel format)" + }, + { + "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2021_03_09.zip", + "name": "default-2021-03-09", + "type": "archive", + "path_in_archive": ".", + "size": 133230419, + "description": "updated default models provided by github.com/qurator-spk (SavedModel format)" + } + ] + } + } +} diff --git a/src/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json index b840005..9eb8932 100644 --- a/src/eynollah/ocrd-tool.json +++ b/src/eynollah/ocrd-tool.json @@ -28,6 +28,16 @@ "type": "boolean", "default": true, "description": "Try to detect all element subtypes, including drop-caps and headings" + }, + "light_version": { + "type": "boolean", + "default": true, + "description": "Try to detect all element subtypes in light version" + }, + "textline_light": { + "type": "boolean", + "default": true, + "description": "Light version need textline light" }, "tables": { "type": "boolean", diff --git a/src/eynollah/ocrd_cli_binarization.py b/src/eynollah/ocrd_cli_binarization.py new file mode 100644 index 0000000..6a8bbdc --- /dev/null +++ b/src/eynollah/ocrd_cli_binarization.py @@ -0,0 +1,158 @@ +from os import environ +from os.path import join +from pathlib import Path +from pkg_resources import resource_string +from json import loads + +from PIL import Image +import numpy as np +import cv2 +from click import command + +from ocrd_utils import ( + getLogger, + assert_file_grp_cardinality, + make_file_id, + MIMETYPE_PAGE +) +from ocrd import Processor +from ocrd_modelfactory import page_from_file +from ocrd_models.ocrd_page import AlternativeImageType, to_xml +from ocrd.decorators import ocrd_cli_options, ocrd_cli_wrap_processor + +from .sbb_binarize import SbbBinarizer + +OCRD_TOOL = loads(resource_string(__name__, 'ocrd-tool-binarization.json').decode('utf8')) +TOOL = 'ocrd-sbb-binarize' + +def cv2pil(img): + return Image.fromarray(img.astype('uint8')) + +def pil2cv(img): + # from ocrd/workspace.py + color_conversion = cv2.COLOR_GRAY2BGR if img.mode in ('1', 'L') else cv2.COLOR_RGB2BGR + pil_as_np_array = np.array(img).astype('uint8') if img.mode == '1' else np.array(img) + return cv2.cvtColor(pil_as_np_array, color_conversion) + +class SbbBinarizeProcessor(Processor): + + def __init__(self, *args, **kwargs): + kwargs['ocrd_tool'] = OCRD_TOOL['tools'][TOOL] + kwargs['version'] = OCRD_TOOL['version'] + super().__init__(*args, **kwargs) + if hasattr(self, 'output_file_grp'): + # processing context + self.setup() + + def setup(self): + """ + Set up the model prior to processing. + """ + LOG = getLogger('processor.SbbBinarize.__init__') + if not 'model' in self.parameter: + raise ValueError("'model' parameter is required") + # resolve relative path via environment variable + model_path = Path(self.parameter['model']) + if not model_path.is_absolute(): + if 'SBB_BINARIZE_DATA' in environ and environ['SBB_BINARIZE_DATA']: + LOG.info("Environment variable SBB_BINARIZE_DATA is set to '%s'" \ + " - prepending to model value '%s'. If you don't want this mechanism," \ + " unset the SBB_BINARIZE_DATA environment variable.", + environ['SBB_BINARIZE_DATA'], model_path) + model_path = Path(environ['SBB_BINARIZE_DATA']).joinpath(model_path) + model_path = model_path.resolve() + if not model_path.is_dir(): + raise FileNotFoundError("Does not exist or is not a directory: %s" % model_path) + # resolve relative path via OCR-D ResourceManager + model_path = self.resolve_resource(str(model_path)) + self.binarizer = SbbBinarizer(model_dir=model_path, logger=LOG) + + def process(self): + """ + Binarize images with sbb_binarization (based on selectional auto-encoders). + + For each page of the input file group, open and deserialize input PAGE-XML + and its respective images. Then iterate over the element hierarchy down to + the requested ``operation_level``. + + For each segment element, retrieve a raw (non-binarized) segment image + according to the layout annotation (from an existing ``AlternativeImage``, + or by cropping into the higher-level images, and deskewing when applicable). + + Pass the image to the binarizer (which runs in fixed-size windows/patches + across the image and stitches the results together). + + Serialize the resulting bilevel image as PNG file and add it to the output + file group (with file ID suffix ``.IMG-BIN``) along with the output PAGE-XML + (referencing it as new ``AlternativeImage`` for the segment element). + + Produce a new PAGE output file by serialising the resulting hierarchy. + """ + LOG = getLogger('processor.SbbBinarize') + assert_file_grp_cardinality(self.input_file_grp, 1) + assert_file_grp_cardinality(self.output_file_grp, 1) + + oplevel = self.parameter['operation_level'] + + for n, input_file in enumerate(self.input_files): + file_id = make_file_id(input_file, self.output_file_grp) + page_id = input_file.pageId or input_file.ID + LOG.info("INPUT FILE %i / %s", n, page_id) + pcgts = page_from_file(self.workspace.download_file(input_file)) + self.add_metadata(pcgts) + pcgts.set_pcGtsId(file_id) + page = pcgts.get_Page() + page_image, page_xywh, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') + + if oplevel == 'page': + LOG.info("Binarizing on 'page' level in page '%s'", page_id) + bin_image = cv2pil(self.binarizer.run(image=pil2cv(page_image), use_patches=True)) + # update METS (add the image file): + bin_image_path = self.workspace.save_image_file(bin_image, + file_id + '.IMG-BIN', + page_id=input_file.pageId, + file_grp=self.output_file_grp) + page.add_AlternativeImage(AlternativeImageType(filename=bin_image_path, comments='%s,binarized' % page_xywh['features'])) + + elif oplevel == 'region': + regions = page.get_AllRegions(['Text', 'Table'], depth=1) + if not regions: + LOG.warning("Page '%s' contains no text/table regions", page_id) + for region in regions: + region_image, region_xywh = self.workspace.image_from_segment(region, page_image, page_xywh, feature_filter='binarized') + region_image_bin = cv2pil(binarizer.run(image=pil2cv(region_image), use_patches=True)) + region_image_bin_path = self.workspace.save_image_file( + region_image_bin, + "%s_%s.IMG-BIN" % (file_id, region.id), + page_id=input_file.pageId, + file_grp=self.output_file_grp) + region.add_AlternativeImage( + AlternativeImageType(filename=region_image_bin_path, comments='%s,binarized' % region_xywh['features'])) + + elif oplevel == 'line': + region_line_tuples = [(r.id, r.get_TextLine()) for r in page.get_AllRegions(['Text'], depth=0)] + if not region_line_tuples: + LOG.warning("Page '%s' contains no text lines", page_id) + for region_id, line in region_line_tuples: + line_image, line_xywh = self.workspace.image_from_segment(line, page_image, page_xywh, feature_filter='binarized') + line_image_bin = cv2pil(binarizer.run(image=pil2cv(line_image), use_patches=True)) + line_image_bin_path = self.workspace.save_image_file( + line_image_bin, + "%s_%s_%s.IMG-BIN" % (file_id, region_id, line.id), + page_id=input_file.pageId, + file_grp=self.output_file_grp) + line.add_AlternativeImage( + AlternativeImageType(filename=line_image_bin_path, comments='%s,binarized' % line_xywh['features'])) + + self.workspace.add_file( + ID=file_id, + file_grp=self.output_file_grp, + pageId=input_file.pageId, + mimetype=MIMETYPE_PAGE, + local_filename=join(self.output_file_grp, file_id + '.xml'), + content=to_xml(pcgts)) + +@command() +@ocrd_cli_options +def cli(*args, **kwargs): + return ocrd_cli_wrap_processor(SbbBinarizeProcessor, *args, **kwargs) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index 1bd190e..ed510c8 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -49,6 +49,8 @@ class EynollahProcessor(Processor): 'curved_line': self.parameter['curved_line'], 'full_layout': self.parameter['full_layout'], 'allow_scaling': self.parameter['allow_scaling'], + 'light_version': self.parameter['light_version'], + 'textline_light': self.parameter['textline_light'], 'headers_off': self.parameter['headers_off'], 'tables': self.parameter['tables'], 'override_dpi': self.parameter['dpi'], From 1ae77e61c854b03c7de29eaf99592186dd19fc74 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:11:36 +0100 Subject: [PATCH 269/412] Update requirements.txt --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f01d319..f4ab5eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,10 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 -tensorflow == 2.12.1 +tensorflow < 2.13 imutils >= 0.5.3 matplotlib setuptools >= 50 +transformers +torch +numba From 22b0b07a733052390ac0b00822f6e662686fbcc7 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 11 Nov 2024 19:01:40 +0100 Subject: [PATCH 270/412] drop capital and marginals extraction is updated --- src/eynollah/eynollah.py | 6 +- src/eynollah/utils/__init__.py | 20 +++++- src/eynollah/utils/marginals.py | 112 ++++++++++++++++++++------------ 3 files changed, 90 insertions(+), 48 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 29d2788..2c3965a 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -249,7 +249,7 @@ class Eynollah: self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" - self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_1__4_3_091124"#"/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" @@ -258,7 +258,7 @@ class Eynollah: self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" - self.model_region_dir_fully = dir_models + "/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/modelens_full_lay_1__4_3_091124"#"/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" if self.textline_light: self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# else: @@ -3653,7 +3653,7 @@ class Eynollah: regions_fully[:,:,0][drops[:,:]==1] = drop_capital_label_in_full_layout_model - regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model) + regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model, text_regions_p) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index 29f80b4..d7f9ccd 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -775,7 +775,7 @@ def put_drop_out_from_only_drop_model(layout_no_patch, layout1): return layout_no_patch -def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop_capital_label): +def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop_capital_label, text_regions_p): drop_only = (layout_in_patch[:, :, 0] == drop_capital_label) * 1 contours_drop, hir_on_drop = return_contours_of_image(drop_only) contours_drop_parent = return_parent_contours(contours_drop, hir_on_drop) @@ -791,12 +791,26 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) + mask_of_drop_cpaital_in_early_layout = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1])) - if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.4: + mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w] = text_regions_p[y : y + h, x : x + w] + + all_drop_capital_pixels_which_is_text_in_early_lo = np.sum( mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w]==1 ) + + mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w]=1 + all_drop_capital_pixels = np.sum(mask_of_drop_cpaital_in_early_layout==1 ) + + percent_text_to_all_in_drop = all_drop_capital_pixels_which_is_text_in_early_lo / float(all_drop_capital_pixels) + + + if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.6 and percent_text_to_all_in_drop>=0.3: layout_in_patch[y : y + h, x : x + w, 0] = drop_capital_label else: - layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = 1#drop_capital_label + layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = drop_capital_label + layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == 0] = drop_capital_label + layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == 4] = drop_capital_label# images + #layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = 1#drop_capital_label return layout_in_patch diff --git a/src/eynollah/utils/marginals.py b/src/eynollah/utils/marginals.py index 984156f..a29e50d 100644 --- a/src/eynollah/utils/marginals.py +++ b/src/eynollah/utils/marginals.py @@ -2,8 +2,6 @@ import numpy as np import cv2 from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d - - from .contour import find_new_features_of_contours, return_contours_of_interested_region from .resize import resize_image from .rotate import rotate_image @@ -123,62 +121,92 @@ def get_marginals(text_with_lines, text_regions, num_col, slope_deskew, light_ve if max_point_of_right_marginal>=text_regions.shape[1]: max_point_of_right_marginal=text_regions.shape[1]-1 + if light_version: + text_regions_org = np.copy(text_regions) + text_regions[text_regions[:,:]==1]=4 + + pixel_img=4 + min_area_text=0.00001 + + polygon_mask_marginals_rotated = return_contours_of_interested_region(mask_marginals,1,min_area_text) + + polygon_mask_marginals_rotated = polygon_mask_marginals_rotated[0] - #plt.imshow(mask_marginals_rotated) - #plt.show() + polygons_of_marginals=return_contours_of_interested_region(text_regions,pixel_img,min_area_text) - text_regions[(mask_marginals_rotated[:,:]!=1) & (text_regions[:,:]==1)]=4 + cx_text_only,cy_text_only ,x_min_text_only,x_max_text_only, y_min_text_only ,y_max_text_only,y_cor_x_min_main=find_new_features_of_contours(polygons_of_marginals) - #plt.imshow(text_regions) - #plt.show() + text_regions[(text_regions[:,:]==4)]=1 - pixel_img=4 - min_area_text=0.00001 - polygons_of_marginals=return_contours_of_interested_region(text_regions,pixel_img,min_area_text) + marginlas_should_be_main_text=[] - cx_text_only,cy_text_only ,x_min_text_only,x_max_text_only, y_min_text_only ,y_max_text_only,y_cor_x_min_main=find_new_features_of_contours(polygons_of_marginals) + x_min_marginals_left=[] + x_min_marginals_right=[] - text_regions[(text_regions[:,:]==4)]=1 + for i in range(len(cx_text_only)): + results = cv2.pointPolygonTest(polygon_mask_marginals_rotated, (cx_text_only[i], cy_text_only[i]), False) - marginlas_should_be_main_text=[] + if results == -1: + marginlas_should_be_main_text.append(polygons_of_marginals[i]) - x_min_marginals_left=[] - x_min_marginals_right=[] - for i in range(len(cx_text_only)): - x_width_mar=abs(x_min_text_only[i]-x_max_text_only[i]) - y_height_mar=abs(y_min_text_only[i]-y_max_text_only[i]) - if x_width_mar>16 and y_height_mar/x_width_mar<18: - marginlas_should_be_main_text.append(polygons_of_marginals[i]) - if x_min_text_only[i]<(mid_point-one_third_left): - x_min_marginals_left_new=x_min_text_only[i] - if len(x_min_marginals_left)==0: - x_min_marginals_left.append(x_min_marginals_left_new) + text_regions_org=cv2.fillPoly(text_regions_org, pts =marginlas_should_be_main_text, color=(4,4)) + text_regions = np.copy(text_regions_org) + + + else: + + text_regions[(mask_marginals_rotated[:,:]!=1) & (text_regions[:,:]==1)]=4 + + pixel_img=4 + min_area_text=0.00001 + + polygons_of_marginals=return_contours_of_interested_region(text_regions,pixel_img,min_area_text) + + cx_text_only,cy_text_only ,x_min_text_only,x_max_text_only, y_min_text_only ,y_max_text_only,y_cor_x_min_main=find_new_features_of_contours(polygons_of_marginals) + + text_regions[(text_regions[:,:]==4)]=1 + + marginlas_should_be_main_text=[] + + x_min_marginals_left=[] + x_min_marginals_right=[] + + for i in range(len(cx_text_only)): + x_width_mar=abs(x_min_text_only[i]-x_max_text_only[i]) + y_height_mar=abs(y_min_text_only[i]-y_max_text_only[i]) + + if x_width_mar>16 and y_height_mar/x_width_mar<18: + marginlas_should_be_main_text.append(polygons_of_marginals[i]) + if x_min_text_only[i]<(mid_point-one_third_left): + x_min_marginals_left_new=x_min_text_only[i] + if len(x_min_marginals_left)==0: + x_min_marginals_left.append(x_min_marginals_left_new) + else: + x_min_marginals_left[0]=min(x_min_marginals_left[0],x_min_marginals_left_new) else: - x_min_marginals_left[0]=min(x_min_marginals_left[0],x_min_marginals_left_new) - else: - x_min_marginals_right_new=x_min_text_only[i] - if len(x_min_marginals_right)==0: - x_min_marginals_right.append(x_min_marginals_right_new) - else: - x_min_marginals_right[0]=min(x_min_marginals_right[0],x_min_marginals_right_new) + x_min_marginals_right_new=x_min_text_only[i] + if len(x_min_marginals_right)==0: + x_min_marginals_right.append(x_min_marginals_right_new) + else: + x_min_marginals_right[0]=min(x_min_marginals_right[0],x_min_marginals_right_new) - if len(x_min_marginals_left)==0: - x_min_marginals_left=[0] - if len(x_min_marginals_right)==0: - x_min_marginals_right=[text_regions.shape[1]-1] + if len(x_min_marginals_left)==0: + x_min_marginals_left=[0] + if len(x_min_marginals_right)==0: + x_min_marginals_right=[text_regions.shape[1]-1] - text_regions=cv2.fillPoly(text_regions, pts =marginlas_should_be_main_text, color=(4,4)) + text_regions=cv2.fillPoly(text_regions, pts =marginlas_should_be_main_text, color=(4,4)) - #text_regions[:,:int(x_min_marginals_left[0])][text_regions[:,:int(x_min_marginals_left[0])]==1]=0 - #text_regions[:,int(x_min_marginals_right[0]):][text_regions[:,int(x_min_marginals_right[0]):]==1]=0 - - - text_regions[:,:int(min_point_of_left_marginal)][text_regions[:,:int(min_point_of_left_marginal)]==1]=0 - text_regions[:,int(max_point_of_right_marginal):][text_regions[:,int(max_point_of_right_marginal):]==1]=0 + #text_regions[:,:int(x_min_marginals_left[0])][text_regions[:,:int(x_min_marginals_left[0])]==1]=0 + #text_regions[:,int(x_min_marginals_right[0]):][text_regions[:,int(x_min_marginals_right[0]):]==1]=0 + + + text_regions[:,:int(min_point_of_left_marginal)][text_regions[:,:int(min_point_of_left_marginal)]==1]=0 + text_regions[:,int(max_point_of_right_marginal):][text_regions[:,int(max_point_of_right_marginal):]==1]=0 ###text_regions[:,0:point_left][text_regions[:,0:point_left]==1]=4 From f43c49c5086289b695322640693a2bf4f5cfa797 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 13 Nov 2024 11:53:56 +0100 Subject: [PATCH 271/412] textlines of drop capitals are connected to corresponding textline if possible otherwise they are inserted in corresponding textregion --- src/eynollah/eynollah.py | 2 +- src/eynollah/utils/drop_capitals.py | 89 ++++++++++++++++++++--------- src/eynollah/writer.py | 6 +- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 2c3965a..d7e389d 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -5176,7 +5176,7 @@ class Eynollah: pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) pixel_lines = 6 if not self.reading_order_machine_based: diff --git a/src/eynollah/utils/drop_capitals.py b/src/eynollah/utils/drop_capitals.py index e12028f..67547d3 100644 --- a/src/eynollah/utils/drop_capitals.py +++ b/src/eynollah/utils/drop_capitals.py @@ -4,6 +4,7 @@ from .contour import ( find_new_features_of_contours, return_contours_of_image, return_parent_contours, + return_contours_of_interested_region, ) def adhere_drop_capital_region_into_corresponding_textline( @@ -17,6 +18,7 @@ def adhere_drop_capital_region_into_corresponding_textline( all_found_textline_polygons_h, kernel=None, curved_line=False, + textline_light=False, ): # print(np.shape(all_found_textline_polygons),np.shape(all_found_textline_polygons[3]),'all_found_textline_polygonsshape') # print(all_found_textline_polygons[3]) @@ -76,7 +78,7 @@ def adhere_drop_capital_region_into_corresponding_textline( # region_with_intersected_drop=region_with_intersected_drop/3 region_with_intersected_drop = region_with_intersected_drop.astype(np.uint8) # print(np.unique(img_con_all_copy[:,:,0])) - if curved_line: + if curved_line or textline_light: if len(region_with_intersected_drop) > 1: sum_pixels_of_intersection = [] @@ -114,12 +116,17 @@ def adhere_drop_capital_region_into_corresponding_textline( img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) img_textlines = img_textlines.astype(np.uint8) - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + contours_combined = return_contours_of_interested_region(img_textlines, 255, 0) + + #plt.imshow(img_textlines) + #plt.show() + + #imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + #ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - # print(len(contours_combined),'len textlines mixed') areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) contours_biggest = contours_combined[np.argmax(areas_cnt_text)] @@ -130,8 +137,13 @@ def adhere_drop_capital_region_into_corresponding_textline( # contours_biggest[:,0,1]=contours_biggest[:,0,1]#-all_box_coord[int(region_final)][0] # contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - - all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + + if len(contours_combined)==1: + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + elif len(contours_combined)==2: + all_found_textline_polygons[int(region_final)].insert(arg_min, polygons_of_drop_capitals[i_drop] ) + else: + pass except: # print('gordun1') @@ -167,14 +179,13 @@ def adhere_drop_capital_region_into_corresponding_textline( img_textlines = img_textlines.astype(np.uint8) - # plt.imshow(img_textlines) - # plt.show() - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + + contours_combined = return_contours_of_interested_region(img_textlines, 255, 0) + ##imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + ##ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + ##contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - # print(len(contours_combined),'len textlines mixed') areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) contours_biggest = contours_combined[np.argmax(areas_cnt_text)] @@ -186,7 +197,12 @@ def adhere_drop_capital_region_into_corresponding_textline( # print(np.shape(contours_biggest),'contours_biggest') # print(np.shape(all_found_textline_polygons[int(region_final)][arg_min])) ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + if len(contours_combined)==1: + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + elif len(contours_combined)==2: + all_found_textline_polygons[int(region_final)].insert(arg_min, polygons_of_drop_capitals[i_drop] ) + else: + pass except: pass @@ -215,10 +231,11 @@ def adhere_drop_capital_region_into_corresponding_textline( img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) img_textlines = img_textlines.astype(np.uint8) - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + contours_combined = return_contours_of_interested_region(img_textlines, 255, 0) + #imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + #ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # print(len(contours_combined),'len textlines mixed') areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) @@ -231,7 +248,12 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest[:, 0, 1] = contours_biggest[:, 0, 1] # -all_box_coord[int(region_final)][0] ##contours_biggest=contours_biggest.reshape(np.shape(contours_biggest)[0],np.shape(contours_biggest)[2]) - all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + if len(contours_combined)==1: + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + elif len(contours_combined)==2: + all_found_textline_polygons[int(region_final)].insert(arg_min, polygons_of_drop_capitals[i_drop] ) + else: + pass # all_found_textline_polygons[int(region_final)][arg_min]=contours_biggest except: @@ -320,10 +342,12 @@ def adhere_drop_capital_region_into_corresponding_textline( img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) img_textlines = img_textlines.astype(np.uint8) - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + contours_combined = return_contours_of_interested_region(img_textlines, 255, 0) + + #imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + #ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # print(len(contours_combined),'len textlines mixed') areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) @@ -336,8 +360,12 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest[:, 0, 1] = contours_biggest[:, 0, 1] - all_box_coord[int(region_final)][0] contours_biggest = contours_biggest.reshape(np.shape(contours_biggest)[0], np.shape(contours_biggest)[2]) - - all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + if len(contours_combined)==1: + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + elif len(contours_combined)==2: + all_found_textline_polygons[int(region_final)].insert(arg_min, polygons_of_drop_capitals[i_drop] ) + else: + pass except: # print('gordun1') @@ -375,10 +403,12 @@ def adhere_drop_capital_region_into_corresponding_textline( img_textlines = cv2.fillPoly(img_textlines, pts=[polygons_of_drop_capitals[i_drop]], color=(255, 255, 255)) img_textlines = img_textlines.astype(np.uint8) - imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + contours_combined = return_contours_of_interested_region(img_textlines, 255, 0) + + #imgray = cv2.cvtColor(img_textlines, cv2.COLOR_BGR2GRAY) + #ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + #contours_combined, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # print(len(contours_combined),'len textlines mixed') areas_cnt_text = np.array([cv2.contourArea(contours_combined[j]) for j in range(len(contours_combined))]) @@ -391,7 +421,12 @@ def adhere_drop_capital_region_into_corresponding_textline( contours_biggest[:, 0, 1] = contours_biggest[:, 0, 1] - all_box_coord[int(region_final)][0] contours_biggest = contours_biggest.reshape(np.shape(contours_biggest)[0], np.shape(contours_biggest)[2]) - all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + if len(contours_combined)==1: + all_found_textline_polygons[int(region_final)][arg_min] = contours_biggest + elif len(contours_combined)==2: + all_found_textline_polygons[int(region_final)].insert(arg_min, polygons_of_drop_capitals[i_drop] ) + else: + pass # all_found_textline_polygons[int(region_final)][arg_min]=contours_biggest except: diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index 96441c6..496b3db 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -285,9 +285,9 @@ class EynollahXmlWriter(): dropcapital = TextRegionType(id=counter.next_region_id, type_='drop-capital', Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_drop_capitals[mm], page_coord))) page.add_TextRegion(dropcapital) - all_box_coord_drop = None - slopes_drop = None - self.serialize_lines_in_dropcapital(dropcapital, [found_polygons_drop_capitals[mm]], mm, page_coord, all_box_coord_drop, slopes_drop, counter, ocr_all_textlines_textregion=None) + ###all_box_coord_drop = None + ###slopes_drop = None + ###self.serialize_lines_in_dropcapital(dropcapital, [found_polygons_drop_capitals[mm]], mm, page_coord, all_box_coord_drop, slopes_drop, counter, ocr_all_textlines_textregion=None) for mm in range(len(found_polygons_text_region_img)): page.add_ImageRegion(ImageRegionType(id=counter.next_region_id, Coords=CoordsType(points=self.calculate_polygon_coords(found_polygons_text_region_img[mm], page_coord)))) From ce5b6112960f67d7819b11a9b346da0d8f5fdb4d Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 14 Nov 2024 17:18:07 +0100 Subject: [PATCH 272/412] tests are passed - new models by the way should be uploaded --- tests/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_run.py b/tests/test_run.py index 2596dad..cdb715a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -2,7 +2,7 @@ from os import environ from pathlib import Path from ocrd_utils import pushd_popd from tests.base import CapturingTestCase as TestCase, main -from eynollah.cli import main as eynollah_cli +from eynollah.cli import layout as eynollah_cli testdir = Path(__file__).parent.resolve() From 5fa8ca46a47be5349ad91ae0f4121cdf661bf466 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 14 Nov 2024 17:35:00 +0100 Subject: [PATCH 273/412] updating requirements --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index f4ab5eb..02450aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,6 @@ tensorflow < 2.13 imutils >= 0.5.3 matplotlib setuptools >= 50 -transformers -torch -numba +transformers <= 4.30.2 +torch <= 2.0.1 +numba <= 0.58.1 From d9f79c3404fb6372031625d357fed5727fa6ec51 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 18 Nov 2024 10:15:19 +0100 Subject: [PATCH 274/412] fixing IndexError by reading order detection --- src/eynollah/eynollah.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index d7e389d..4f9eaa6 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -2678,17 +2678,29 @@ class Eynollah: try: arg_text_con = [] for ii in range(len(cx_text_only)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if (x_min_text_only[ii] + 80) >= boxes[jj][0] and (x_min_text_only[ii] + 80) < boxes[jj][1] and y_cor_x_min_main[ii] >= boxes[jj][2] and y_cor_x_min_main[ii] < boxes[jj][3]: arg_text_con.append(jj) + check_if_textregion_located_in_a_box = True break + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) arg_text_con_h = [] for ii in range(len(cx_text_only_h)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if (x_min_text_only_h[ii] + 80) >= boxes[jj][0] and (x_min_text_only_h[ii] + 80) < boxes[jj][1] and y_cor_x_min_main_h[ii] >= boxes[jj][2] and y_cor_x_min_main_h[ii] < boxes[jj][3]: arg_text_con_h.append(jj) + check_if_textregion_located_in_a_box = True break + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con_h.append(ind_min) args_contours_h = np.array(range(len(arg_text_con_h))) order_by_con_head = np.zeros(len(arg_text_con_h)) @@ -2742,15 +2754,22 @@ class Eynollah: order_text_new = [] for iii in range(len(order_of_texts_tot)): order_text_new.append(np.where(np.array(order_of_texts_tot) == iii)[0][0]) - + except Exception as why: self.logger.error(why) arg_text_con = [] for ii in range(len(cx_text_only)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) + check_if_textregion_located_in_a_box = True break + + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) order_by_con_main = np.zeros(len(arg_text_con)) @@ -2759,10 +2778,16 @@ class Eynollah: arg_text_con_h = [] for ii in range(len(cx_text_only_h)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if cx_text_only_h[ii] >= boxes[jj][0] and cx_text_only_h[ii] < boxes[jj][1] and cy_text_only_h[ii] >= boxes[jj][2] and cy_text_only_h[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con_h.append(jj) + check_if_textregion_located_in_a_box = True break + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con_h.append(ind_min) args_contours_h = np.array(range(len(arg_text_con_h))) order_by_con_head = np.zeros(len(arg_text_con_h)) @@ -2814,6 +2839,7 @@ class Eynollah: order_text_new = [] for iii in range(len(order_of_texts_tot)): order_text_new.append(np.where(np.array(order_of_texts_tot) == iii)[0][0]) + return order_text_new, id_of_texts_tot def do_order_of_regions_no_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -2823,10 +2849,16 @@ class Eynollah: try: arg_text_con = [] for ii in range(len(cx_text_only)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if (x_min_text_only[ii] + 80) >= boxes[jj][0] and (x_min_text_only[ii] + 80) < boxes[jj][1] and y_cor_x_min_main[ii] >= boxes[jj][2] and y_cor_x_min_main[ii] < boxes[jj][3]: arg_text_con.append(jj) + check_if_textregion_located_in_a_box = True break + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) order_by_con_main = np.zeros(len(arg_text_con)) @@ -2868,10 +2900,16 @@ class Eynollah: self.logger.error(why) arg_text_con = [] for ii in range(len(cx_text_only)): + check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) + check_if_textregion_located_in_a_box = True break + if not check_if_textregion_located_in_a_box: + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + ind_min = np.argmin(dists_tr_from_box) + arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) order_by_con_main = np.zeros(len(arg_text_con)) From b622494f34f8366f21ded3f3c8b85b8519245fa7 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 21 Nov 2024 02:16:22 +0100 Subject: [PATCH 275/412] new table detection model is integrated --- src/eynollah/eynollah.py | 427 +++++++++++++++++++++++---------------- 1 file changed, 248 insertions(+), 179 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 4f9eaa6..f2426f8 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -264,9 +264,13 @@ class Eynollah: else: self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" if self.ocr: - self.model_ocr_dir = dir_models + "/checkpoint-166692_printed_trocr" + self.model_ocr_dir = dir_models + "/trocr_model_ens_of_3_checkpoints_201124" - self.model_tables = dir_models + "/eynollah-tables_20210319" + if self.tables: + if self.light_version: + self.model_table_dir = dir_models + "/modelens_table_0t4_201124" + else: + self.model_table_dir = dir_models + "/eynollah-tables_20210319" self.models = {} @@ -290,6 +294,9 @@ class Eynollah: self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten")#("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") + if self.tables: + self.model_table = self.our_load_model(self.model_table_dir) + self.ls_imgs = os.listdir(self.dir_in) @@ -325,9 +332,13 @@ class Eynollah: self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) - + if self.tables: + self.model_table = self.our_load_model(self.model_table_dir) + self.ls_imgs = os.listdir(self.dir_in) + + def _cache_images(self, image_filename=None, image_pil=None): ret = {} @@ -2326,8 +2337,23 @@ class Eynollah: ###img_bin = np.copy(prediction_bin) ###else: ###img_bin = np.copy(img_resized) - - img_bin = np.copy(img_resized) + if self.ocr and not self.input_binary: + if not self.dir_in: + model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) + else: + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + prediction_bin=prediction_bin[:,:,0] + prediction_bin = (prediction_bin[:,:]==0)*1 + prediction_bin = prediction_bin*255 + + prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) + + prediction_bin = prediction_bin.astype(np.uint16) + #img= np.copy(prediction_bin) + img_bin = np.copy(prediction_bin) + else: + img_bin = np.copy(img_resized) #print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) @@ -3175,91 +3201,101 @@ class Eynollah: img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - model_region, session_region = self.start_new_session_and_model(self.model_tables) + + + if self.dir_in: + pass + else: + self.model_table, _ = self.start_new_session_and_model(self.model_table_dir) patches = False - if num_col_classifier < 4 and num_col_classifier > 2: - prediction_table = self.do_prediction(patches, img, model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), model_region) - pre_updown = cv2.flip(pre_updown, -1) - - prediction_table[:,:,0][pre_updown[:,:,0]==1]=1 + if self.light_version: + prediction_table = self.do_prediction_new_concept(patches, img, self.model_table) prediction_table = prediction_table.astype(np.int16) - - elif num_col_classifier ==2: - height_ext = 0#int( img.shape[0]/4. ) - h_start = int(height_ext/2.) - width_ext = int( img.shape[1]/8. ) - w_start = int(width_ext/2.) - - height_new = img.shape[0]+height_ext - width_new = img.shape[1]+width_ext - - img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 - img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] - - prediction_ext = self.do_prediction(patches, img_new, model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) - pre_updown = cv2.flip(pre_updown, -1) - - prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - - prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 - prediction_table = prediction_table.astype(np.int16) - - elif num_col_classifier ==1: - height_ext = 0# int( img.shape[0]/4. ) - h_start = int(height_ext/2.) - width_ext = int( img.shape[1]/4. ) - w_start = int(width_ext/2.) - - height_new = img.shape[0]+height_ext - width_new = img.shape[1]+width_ext - - img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 - img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] - - prediction_ext = self.do_prediction(patches, img_new, model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), model_region) - pre_updown = cv2.flip(pre_updown, -1) - - prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - - prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 - prediction_table = prediction_table.astype(np.int16) - + return prediction_table[:,:,0] else: - prediction_table = np.zeros(img.shape) - img_w_half = int(img.shape[1]/2.) + if num_col_classifier < 4 and num_col_classifier > 2: + prediction_table = self.do_prediction(patches, img, self.model_table) + pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), self.model_table) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table[:,:,0][pre_updown[:,:,0]==1]=1 + prediction_table = prediction_table.astype(np.int16) + + elif num_col_classifier ==2: + height_ext = 0#int( img.shape[0]/4. ) + h_start = int(height_ext/2.) + width_ext = int( img.shape[1]/8. ) + w_start = int(width_ext/2.) + + height_new = img.shape[0]+height_ext + width_new = img.shape[1]+width_ext + + img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 + img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] - pre1 = self.do_prediction(patches, img[:,0:img_w_half,:], model_region) - pre2 = self.do_prediction(patches, img[:,img_w_half:,:], model_region) - pre_full = self.do_prediction(patches, img[:,:,:], model_region) - pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), model_region) - pre_updown = cv2.flip(pre_updown, -1) - - prediction_table_full_erode = cv2.erode(pre_full[:,:,0], KERNEL, iterations=4) - prediction_table_full_erode = cv2.dilate(prediction_table_full_erode, KERNEL, iterations=4) - - prediction_table_full_updown_erode = cv2.erode(pre_updown[:,:,0], KERNEL, iterations=4) - prediction_table_full_updown_erode = cv2.dilate(prediction_table_full_updown_erode, KERNEL, iterations=4) + prediction_ext = self.do_prediction(patches, img_new, self.model_table) + pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), self.model_table) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + + prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 + prediction_table = prediction_table.astype(np.int16) - prediction_table[:,0:img_w_half,:] = pre1[:,:,:] - prediction_table[:,img_w_half:,:] = pre2[:,:,:] + elif num_col_classifier ==1: + height_ext = 0# int( img.shape[0]/4. ) + h_start = int(height_ext/2.) + width_ext = int( img.shape[1]/4. ) + w_start = int(width_ext/2.) - prediction_table[:,:,0][prediction_table_full_erode[:,:]==1]=1 - prediction_table[:,:,0][prediction_table_full_updown_erode[:,:]==1]=1 - prediction_table = prediction_table.astype(np.int16) + height_new = img.shape[0]+height_ext + width_new = img.shape[1]+width_ext + + img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 + img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] + + prediction_ext = self.do_prediction(patches, img_new, self.model_table) + pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), self.model_table) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + + prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 + prediction_table = prediction_table.astype(np.int16) + + else: + prediction_table = np.zeros(img.shape) + img_w_half = int(img.shape[1]/2.) + + pre1 = self.do_prediction(patches, img[:,0:img_w_half,:], self.model_table) + pre2 = self.do_prediction(patches, img[:,img_w_half:,:], self.model_table) + pre_full = self.do_prediction(patches, img[:,:,:], self.model_table) + pre_updown = self.do_prediction(patches, cv2.flip(img[:,:,:], -1), self.model_table) + pre_updown = cv2.flip(pre_updown, -1) + + prediction_table_full_erode = cv2.erode(pre_full[:,:,0], KERNEL, iterations=4) + prediction_table_full_erode = cv2.dilate(prediction_table_full_erode, KERNEL, iterations=4) + + prediction_table_full_updown_erode = cv2.erode(pre_updown[:,:,0], KERNEL, iterations=4) + prediction_table_full_updown_erode = cv2.dilate(prediction_table_full_updown_erode, KERNEL, iterations=4) + + prediction_table[:,0:img_w_half,:] = pre1[:,:,:] + prediction_table[:,img_w_half:,:] = pre2[:,:,:] + + prediction_table[:,:,0][prediction_table_full_erode[:,:]==1]=1 + prediction_table[:,:,0][prediction_table_full_updown_erode[:,:]==1]=1 + prediction_table = prediction_table.astype(np.int16) + + #prediction_table_erode = cv2.erode(prediction_table[:,:,0], self.kernel, iterations=6) + #prediction_table_erode = cv2.dilate(prediction_table_erode, self.kernel, iterations=6) - #prediction_table_erode = cv2.erode(prediction_table[:,:,0], self.kernel, iterations=6) - #prediction_table_erode = cv2.dilate(prediction_table_erode, self.kernel, iterations=6) - - prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) - prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) - return prediction_table_erode.astype(np.int16) + prediction_table_erode = cv2.erode(prediction_table[:,:,0], KERNEL, iterations=20) + prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) + return prediction_table_erode.astype(np.int16) def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light): #print(text_regions_p_1.shape, 'text_regions_p_1 shape run graphics') @@ -3500,49 +3536,62 @@ class Eynollah: #print(time.time()-t_0_box,'time box in 3.1') if self.tables: - text_regions_p_tables = np.copy(text_regions_p) - text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) - #print(time.time()-t_0_box,'time box in 3.2') - img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) - #print(time.time()-t_0_box,'time box in 3.3') + if self.light_version: + pass + else: + text_regions_p_tables = np.copy(text_regions_p) + text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) + #print(time.time()-t_0_box,'time box in 3.2') + img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) + #print(time.time()-t_0_box,'time box in 3.3') else: boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) if self.tables: - text_regions_p_tables = np.copy(text_regions_p_1_n) - text_regions_p_tables =np.round(text_regions_p_tables) - text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 - - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) - img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) - - img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) - img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) - img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) - img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) + if self.light_version: + pass + else: + text_regions_p_tables = np.copy(text_regions_p_1_n) + text_regions_p_tables =np.round(text_regions_p_tables) + text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 + + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) + + img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) + img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) + img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) + img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) #print(time.time()-t_0_box,'time box in 4') self.logger.info("detecting boxes took %.1fs", time.time() - t1) if self.tables: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - img_revised_tab = np.copy(img_revised_tab2[:,:,0]) - img_revised_tab[:,:][(text_regions_p[:,:] == 1) & (img_revised_tab[:,:] != 10)] = 1 + if self.light_version: + text_regions_p[:,:][table_prediction[:,:]==1] = 10 + img_revised_tab=text_regions_p[:,:] else: - img_revised_tab = np.copy(text_regions_p[:,:]) - img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 - img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 - - text_regions_p[:,:][text_regions_p[:,:]==10] = 0 - text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + img_revised_tab = np.copy(img_revised_tab2[:,:,0]) + img_revised_tab[:,:][(text_regions_p[:,:] == 1) & (img_revised_tab[:,:] != 10)] = 1 + else: + img_revised_tab = np.copy(text_regions_p[:,:]) + img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 + img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 + + text_regions_p[:,:][text_regions_p[:,:]==10] = 0 + text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 else: img_revised_tab=text_regions_p[:,:] #img_revised_tab = text_regions_p[:, :] - polygons_of_images = return_contours_of_interested_region(img_revised_tab, 2) + if self.light_version: + polygons_of_images = return_contours_of_interested_region(text_regions_p, 2) + else: + polygons_of_images = return_contours_of_interested_region(img_revised_tab, 2) pixel_img = 4 min_area_mar = 0.00001 @@ -3565,82 +3614,102 @@ class Eynollah: self.logger.debug('enter run_boxes_full_layout') t_full0 = time.time() if self.tables: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) - - text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) - textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) - table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) - - regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 - regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 - else: - text_regions_p_1_n = None - textline_mask_tot_d = None - regions_without_separators_d = None - - regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) - regions_without_separators[table_prediction == 1] = 1 - - pixel_lines=3 - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) + if self.light_version: + text_regions_p[:,:][table_prediction[:,:]==1] = 10 + img_revised_tab=text_regions_p[:,:] + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + + text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) + textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) + table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) + + regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 + else: + text_regions_p_1_n = None + textline_mask_tot_d = None + regions_without_separators_d = None + regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators[table_prediction == 1] = 1 - if num_col_classifier>=3: + else: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + + text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) + textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) + table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) + + regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 + else: + text_regions_p_1_n = None + textline_mask_tot_d = None + regions_without_separators_d = None + + regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators[table_prediction == 1] = 1 + + pixel_lines=3 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:,:], KERNEL, iterations=6) + num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:,:], KERNEL, iterations=6) - else: - pass - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - text_regions_p_tables = np.copy(text_regions_p) - text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) - - img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) - - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - text_regions_p_tables = np.copy(text_regions_p_1_n) - text_regions_p_tables = np.round(text_regions_p_tables) - text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 - - pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) - - img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction_n, 10, num_col_classifier) - img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) - + num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) - img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) - img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) - - img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) - - - if np.abs(slope_deskew) < 0.13: - img_revised_tab = np.copy(img_revised_tab2[:,:,0]) - else: - img_revised_tab = np.copy(text_regions_p[:,:]) - img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 - img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 + if num_col_classifier>=3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:,:], KERNEL, iterations=6) + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:,:], KERNEL, iterations=6) + else: + pass + + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + text_regions_p_tables = np.copy(text_regions_p) + text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) - ##img_revised_tab=img_revised_tab2[:,:,0] - #img_revised_tab=text_regions_p[:,:] - text_regions_p[:,:][text_regions_p[:,:]==10] = 0 - text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 - #img_revised_tab[img_revised_tab2[:,:,0]==10] =10 + img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) + + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + text_regions_p_tables = np.copy(text_regions_p_1_n) + text_regions_p_tables = np.round(text_regions_p_tables) + text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 + + pixel_line = 3 + img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction_n, 10, num_col_classifier) + img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) + + + img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) + img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) + + img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) + + + if np.abs(slope_deskew) < 0.13: + img_revised_tab = np.copy(img_revised_tab2[:,:,0]) + else: + img_revised_tab = np.copy(text_regions_p[:,:]) + img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 + img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 + + + ##img_revised_tab=img_revised_tab2[:,:,0] + #img_revised_tab=text_regions_p[:,:] + text_regions_p[:,:][text_regions_p[:,:]==10] = 0 + text_regions_p[:,:][img_revised_tab[:,:]==10] = 10 + #img_revised_tab[img_revised_tab2[:,:,0]==10] =10 pixel_img = 4 min_area_mar = 0.00001 From 1746920275a759e06efa5a862dc39b898e4db75c Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 21 Nov 2024 12:08:29 +0100 Subject: [PATCH 276/412] Update Makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a3b7b95..454c75e 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,9 @@ models_eynollah: models_eynollah.tar.gz models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' - wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' + # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' # Install with pip install: From 3000255a243105ed82ae5059117c43d6bc93f31d Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 22 Nov 2024 12:40:21 +0100 Subject: [PATCH 277/412] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 454c75e..6089f6e 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ models_eynollah: models_eynollah.tar.gz models_eynollah.tar.gz: # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed_savedmodel.tar.gz' + wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah.tar.gz' # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' From 8014a9e416dd4bf80f4047d55644844d4d75293a Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 22 Nov 2024 19:47:06 +0100 Subject: [PATCH 278/412] Update Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6089f6e..506fcf7 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ install-dev: pip install -e . smoke-test: - eynollah -i tests/resources/kant_aufklaerung_1784_0020.tif -o . -m $(PWD)/models_eynollah + eynollah layout -i tests/resources/kant_aufklaerung_1784_0020.tif -o . -m $(PWD)/models_eynollah # Run unit tests test: From 1083d1c7fb48d9182e6f635b6815dbc34b145e24 Mon Sep 17 00:00:00 2001 From: kba Date: Mon, 25 Nov 2024 19:32:42 +0100 Subject: [PATCH 279/412] gha: try to free disk space --- .github/workflows/test-eynollah.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 3a33dcf..8a6941f 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -14,6 +14,12 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: + - name: clean up + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" - uses: actions/checkout@v4 - uses: actions/cache@v4 id: model_cache From 6aad006f4c556b33a1d23d83c20fe2ca112448bc Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 2 Dec 2024 12:43:57 +0100 Subject: [PATCH 280/412] filter textregions without textline --- src/eynollah/eynollah.py | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index f2426f8..c28c441 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4772,8 +4772,45 @@ class Eynollah: - + def filter_contours_without_textline_inside(self,contours,text_con_org, contours_textline): + ###contours_txtline_of_all_textregions = [] + + ###for jj in range(len(contours_textline)): + ###contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours_textline[jj] + + ###M_main_textline = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] + ###cx_main_textline = [(M_main_textline[j]["m10"] / (M_main_textline[j]["m00"] + 1e-32)) for j in range(len(M_main_textline))] + ###cy_main_textline = [(M_main_textline[j]["m01"] / (M_main_textline[j]["m00"] + 1e-32)) for j in range(len(M_main_textline))] + + + ###M_main = [cv2.moments(contours[j]) for j in range(len(contours))] + ###cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + ###cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + + ###contours_with_textline = [] + ###for ind_tr, con_tr in enumerate(contours): + ###results = [cv2.pointPolygonTest(con_tr, (cx_main_textline[index_textline_con], cy_main_textline[index_textline_con]), False) for index_textline_con in range(len(contours_txtline_of_all_textregions)) ] + + ###results = np.array(results) + ###if np.any(results==1): + ###contours_with_textline.append(con_tr) + + textregion_index_to_del = [] + for index_textregion, textlines_textregion in enumerate(contours_textline): + if len(textlines_textregion)==0: + textregion_index_to_del.append(index_textregion) + + uniqe_args_trs = np.unique(textregion_index_to_del) + uniqe_args_trs_sorted = np.sort(uniqe_args_trs)[::-1] + + + for ind_u_a_trs in uniqe_args_trs_sorted: + contours.pop(ind_u_a_trs) + contours_textline.pop(ind_u_a_trs) + text_con_org.pop(ind_u_a_trs) + + return contours, text_con_org, contours_textline def dilate_textlines(self,all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): @@ -5239,6 +5276,8 @@ class Eynollah: all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) + else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) @@ -5395,17 +5434,17 @@ class Eynollah: if self.textline_light: mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 img_croped = img_poly_on_img[y:y+h, x:x+w, :] + #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) ocr_textline_in_textregion.append(text_ocr) - ##cv2.imwrite(str(ind_tot)+'.png', img_croped) + ind_tot = ind_tot +1 ocr_all_textlines.append(ocr_textline_in_textregion) From 871d7bfc5a76d8a81b4aec0b4b7c701eeeb883f9 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 4 Dec 2024 16:41:00 +0100 Subject: [PATCH 281/412] fixed: machine based reading order cause tuple index out of range error if number of textregion is one. --- src/eynollah/eynollah.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index c28c441..e802e29 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4013,7 +4013,10 @@ class Eynollah: inference_bs = 3 input_1= np.zeros( (inference_bs, height1, width1,3)) starting_list_of_regions = [] - starting_list_of_regions.append( list(range(labels_con.shape[2])) ) + if len(co_text_all)<=1: + starting_list_of_regions.append( list(range(1)) ) + else: + starting_list_of_regions.append( list(range(labels_con.shape[2])) ) index_update = 0 index_selected = starting_list_of_regions[0] #print(labels_con.shape[2],"number of regions for reading order") From f765e2603b14186574ec86ff70a1767adfec867d Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 15:57:13 +0000 Subject: [PATCH 282/412] move Torch to optional dependencies (to avoid clash with TF over CuDNN) --- pyproject.toml | 4 ++++ requirements.txt | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b056cb7..61d488a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,10 @@ classifiers = [ "Topic :: Scientific/Engineering :: Image Processing", ] +[project.optional-dependencies] +OCR = ["torch <= 2.0.1", "transformers <= 4.30.2"] +plotting = ["matplotlib"] + [project.scripts] eynollah = "eynollah.cli:main" ocrd-eynollah-segment = "eynollah.ocrd_cli:main" diff --git a/requirements.txt b/requirements.txt index 02450aa..d72df29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,4 @@ numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 imutils >= 0.5.3 -matplotlib -setuptools >= 50 -transformers <= 4.30.2 -torch <= 2.0.1 numba <= 0.58.1 From 7ae64f3717ac84f7aebc12c5933015d8bc4b8056 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 16:18:35 +0000 Subject: [PATCH 283/412] RO model: do not reload when in dir_in mode --- src/eynollah/eynollah.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index e802e29..2dd5505 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -255,7 +255,7 @@ class Eynollah: self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_region_dir_p_ens_light_only_images_extraction = dir_models + "/eynollah-main-regions_20231127_672_org_ens_11_13_16_17_18" - self.model_reading_order_machine_dir = dir_models + "/model_ens_reading_order_machine_based" + self.model_reading_order_dir = dir_models + "/model_ens_reading_order_machine_based" self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" self.model_region_dir_fully = dir_models + "/modelens_full_lay_1__4_3_091124"#"/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" @@ -289,7 +289,7 @@ class Eynollah: ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) - self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) + self.model_reading_order = self.our_load_model(self.model_reading_order_dir) if self.ocr: self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") @@ -331,7 +331,7 @@ class Eynollah: self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) self.model_region_fl = self.our_load_model(self.model_region_dir_fully) self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) - self.model_reading_order_machine = self.our_load_model(self.model_reading_order_machine_dir) + self.model_reading_order = self.our_load_model(self.model_reading_order_dir) if self.tables: self.model_table = self.our_load_model(self.model_table_dir) @@ -3804,7 +3804,7 @@ class Eynollah: model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model - def do_order_of_regions_with_machine(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): + def do_order_of_regions_with_model(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] @@ -3818,7 +3818,8 @@ class Eynollah: img_poly[text_regions_p[:,:]==3] = 4 img_poly[text_regions_p[:,:]==6] = 5 - model_ro_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) + if not self.dir_in: + self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) height1 =672#448 width1 = 448#224 @@ -3896,7 +3897,7 @@ class Eynollah: batch_counter = batch_counter+1 if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): - y_pr=model_ro_machine.predict(input_1 , verbose=0) + y_pr = self.model_reading_order.predict(input_1 , verbose=0) if batch_counter==inference_bs: iteration_batches = inference_bs @@ -3952,7 +3953,7 @@ class Eynollah: else: early_list_bigger_than_one = -20 return list_inp, early_list_bigger_than_one - def do_order_of_regions_with_machine_optimized_algorithm(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): + def do_order_of_regions_with_model_optimized_algorithm(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] @@ -3969,7 +3970,7 @@ class Eynollah: if self.dir_in: pass else: - self.model_reading_order_machine, _ = self.start_new_session_and_model(self.model_reading_order_machine_dir) + self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) height1 =672#448 width1 = 448#224 @@ -4055,7 +4056,7 @@ class Eynollah: batch_counter = batch_counter+1 if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): - y_pr=self.model_reading_order_machine.predict(input_1 , verbose=0) + y_pr = self.model_reading_order.predict(input_1 , verbose=0) if batch_counter==inference_bs: iteration_batches = inference_bs @@ -5362,7 +5363,7 @@ class Eynollah: if self.full_layout: if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) @@ -5384,7 +5385,7 @@ class Eynollah: else: contours_only_text_parent_h = None if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_machine_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) From 3b9a29bc5c187fe6ae4c41450a0095c3271ec703 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 18:19:54 +0000 Subject: [PATCH 284/412] simplify dir_in conditionals --- src/eynollah/eynollah.py | 78 +++++++++++++--------------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 2dd5505..c1e0f4d 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -274,7 +274,8 @@ class Eynollah: self.models = {} - if dir_in and light_version: + if dir_in: + # as in start_new_session: config = tf.compat.v1.ConfigProto() config.gpu_options.allow_growth = True session = tf.compat.v1.Session(config=config) @@ -283,62 +284,31 @@ class Eynollah: self.model_page = self.our_load_model(self.model_page_dir) self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) self.model_bin = self.our_load_model(self.model_dir_of_binarization) - self.model_textline = self.our_load_model(self.model_textline_dir) - self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) - self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) - ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) - self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) - self.model_region_fl = self.our_load_model(self.model_region_dir_fully) - self.model_reading_order = self.our_load_model(self.model_reading_order_dir) - if self.ocr: - self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten")#("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") - if self.tables: - self.model_table = self.our_load_model(self.model_table_dir) - + if self.extract_only_images: + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) + else: + self.model_textline = self.our_load_model(self.model_textline_dir) + if self.light_version: + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) + self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) + else: + self.model_region = self.our_load_model(self.model_region_dir_p_ens) + self.model_region_p2 = self.our_load_model(self.model_region_dir_p2) + self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) + ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) + self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) + self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + if self.reading_order_machine_based: + self.model_reading_order = self.our_load_model(self.model_reading_order_dir) + if self.ocr: + self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten")#("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") + if self.tables: + self.model_table = self.our_load_model(self.model_table_dir) self.ls_imgs = os.listdir(self.dir_in) - if dir_in and self.extract_only_images: - config = tf.compat.v1.ConfigProto() - config.gpu_options.allow_growth = True - session = tf.compat.v1.Session(config=config) - set_session(session) - - self.model_page = self.our_load_model(self.model_page_dir) - self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) - self.model_bin = self.our_load_model(self.model_dir_of_binarization) - #self.model_textline = self.our_load_model(self.model_textline_dir) - self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) - #self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) - #self.model_region_fl = self.our_load_model(self.model_region_dir_fully) - - self.ls_imgs = os.listdir(self.dir_in) - - if dir_in and not (light_version or self.extract_only_images): - config = tf.compat.v1.ConfigProto() - config.gpu_options.allow_growth = True - session = tf.compat.v1.Session(config=config) - set_session(session) - - self.model_page = self.our_load_model(self.model_page_dir) - self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) - self.model_bin = self.our_load_model(self.model_dir_of_binarization) - self.model_textline = self.our_load_model(self.model_textline_dir) - self.model_region = self.our_load_model(self.model_region_dir_p_ens) - self.model_region_p2 = self.our_load_model(self.model_region_dir_p2) - self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) - self.model_region_fl = self.our_load_model(self.model_region_dir_fully) - self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) - self.model_reading_order = self.our_load_model(self.model_reading_order_dir) - if self.tables: - self.model_table = self.our_load_model(self.model_table_dir) - - self.ls_imgs = os.listdir(self.dir_in) - - - def _cache_images(self, image_filename=None, image_pil=None): ret = {} From 329fac23f67b2a46fe3c00b0340bd3a56143bed2 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 18:29:49 +0000 Subject: [PATCH 285/412] do not reload enhancement model in dir_in mode, simplify --- src/eynollah/eynollah.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index c1e0f4d..145f722 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -363,10 +363,11 @@ class Eynollah: def predict_enhancement(self, img): self.logger.debug("enter predict_enhancement") - model_enhancement, session_enhancement = self.start_new_session_and_model(self.model_dir_of_enhancement) + if not self.dir_in: + self.model_enhancement, _ = self.start_new_session_and_model(self.model_dir_of_enhancement) - img_height_model = model_enhancement.layers[len(model_enhancement.layers) - 1].output_shape[1] - img_width_model = model_enhancement.layers[len(model_enhancement.layers) - 1].output_shape[2] + img_height_model = self.model_enhancement.layers[len(self.model_enhancement.layers) - 1].output_shape[1] + img_width_model = self.model_enhancement.layers[len(self.model_enhancement.layers) - 1].output_shape[2] if img.shape[0] < img_height_model: img = cv2.resize(img, (img.shape[1], img_width_model), interpolation=cv2.INTER_NEAREST) @@ -409,9 +410,8 @@ class Eynollah: index_y_u = img_h index_y_d = img_h - img_height_model - img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - label_p_pred = model_enhancement.predict(img_patch.reshape(1, img_patch.shape[0], img_patch.shape[1], img_patch.shape[2]), - verbose=0) + img_patch = img[np.newaxis, index_y_d:index_y_u, index_x_d:index_x_u, :] + label_p_pred = self.model_enhancement.predict(img_patch, verbose=0) seg = label_p_pred[0, :, :, :] seg = seg * 255 From 14beb46224b3f0660f44dafbf4bbe094b68d7274 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 21:07:26 +0000 Subject: [PATCH 286/412] simplify loading models w/o dir_in mode --- src/eynollah/eynollah.py | 203 +++++++++++++++------------------------ 1 file changed, 75 insertions(+), 128 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 145f722..d11531a 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -567,7 +567,8 @@ class Eynollah: _, page_coord = self.early_page_for_num_of_column_classification(img) if not self.dir_in: - model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) + self.model_classifier, _ = self.start_new_session_and_model(self.model_dir_of_col_classifier) + if self.input_binary: img_in = np.copy(img) img_in = img_in / 255.0 @@ -590,10 +591,7 @@ class Eynollah: img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] - if not self.dir_in: - label_p_pred = model_num_classifier.predict(img_in, verbose=0) - else: - label_p_pred = self.model_classifier.predict(img_in, verbose=0) + label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 @@ -613,12 +611,10 @@ class Eynollah: self.logger.info("Detected %s DPI", dpi) if self.input_binary: img = self.imread() - if self.dir_in: - prediction_bin = self.do_prediction(True, img, self.model_bin, n_batch_inference=5) - else: - - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img, model_bin, n_batch_inference=5) + if not self.dir_in: + self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) + + prediction_bin = self.do_prediction(True, img, self.model_bin, n_batch_inference=5) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 @@ -641,7 +637,7 @@ class Eynollah: self.page_coord = page_coord if not self.dir_in: - model_num_classifier, session_col_classifier = self.start_new_session_and_model(self.model_dir_of_col_classifier) + self.model_classifier, _ = self.start_new_session_and_model(self.model_dir_of_col_classifier) if self.num_col_upper and not self.num_col_lower: num_col = self.num_col_upper @@ -669,10 +665,7 @@ class Eynollah: img_in[0, :, :, 2] = img_1ch[:, :] - if self.dir_in: - label_p_pred = self.model_classifier.predict(img_in, verbose=0) - else: - label_p_pred = model_num_classifier.predict(img_in, verbose=0) + label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 elif (self.num_col_upper and self.num_col_lower) and (self.num_col_upper!=self.num_col_lower): if self.input_binary: @@ -693,10 +686,7 @@ class Eynollah: img_in[0, :, :, 2] = img_1ch[:, :] - if self.dir_in: - label_p_pred = self.model_classifier.predict(img_in, verbose=0) - else: - label_p_pred = model_num_classifier.predict(img_in, verbose=0) + label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 if num_col > self.num_col_upper: @@ -1381,12 +1371,9 @@ class Eynollah: img = cv2.GaussianBlur(self.image, (5, 5), 0) if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) - if not self.dir_in: - img_page_prediction = self.do_prediction(False, img, model_page) - else: - img_page_prediction = self.do_prediction(False, img, self.model_page) + img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) thresh = cv2.dilate(thresh, KERNEL, iterations=3) @@ -1429,13 +1416,10 @@ class Eynollah: else: img = self.imread() if not self.dir_in: - model_page, session_page = self.start_new_session_and_model(self.model_page_dir) + self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(img, (5, 5), 0) - if self.dir_in: - img_page_prediction = self.do_prediction(False, img, self.model_page) - else: - img_page_prediction = self.do_prediction(False, img, model_page) + img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -1462,9 +1446,12 @@ class Eynollah: img_height_h = img.shape[0] img_width_h = img.shape[1] if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully if patches else self.model_region_dir_fully_np) - else: - model_region = self.model_region_fl if patches else self.model_region_fl_np + if patches: + self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) + else: + self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) + + model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: if self.light_version: @@ -1546,9 +1533,12 @@ class Eynollah: img_height_h = img.shape[0] img_width_h = img.shape[1] if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_fully if patches else self.model_region_dir_fully_np) - else: - model_region = self.model_region_fl if patches else self.model_region_fl_np + if patches: + self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) + else: + self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) + + model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: img = otsu_copy_binary(img) @@ -2049,26 +2039,18 @@ class Eynollah: else: thresholding_for_artificial_class_in_light_version = False if not self.dir_in: - model_textline, session_textline = self.start_new_session_and_model(self.model_textline_dir) + self.model_textline, _ = self.start_new_session_and_model(self.model_textline_dir) #img = img.astype(np.uint8) img_org = np.copy(img) img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - - #if not thresholding_for_artificial_class_in_light_version: - #if num_col_classifier==1: - #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) - #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 - else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - #if not thresholding_for_artificial_class_in_light_version: - #if num_col_classifier==1: - #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) - #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 + prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + #if not thresholding_for_artificial_class_in_light_version: + #if num_col_classifier==1: + #prediction_textline_nopatch = self.do_prediction(False, img, self.model_textline) + #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 prediction_textline = resize_image(prediction_textline, img_h, img_w) textline_mask_tot_ea_art = (prediction_textline[:,:]==2)*1 @@ -2092,10 +2074,7 @@ class Eynollah: if not thresholding_for_artificial_class_in_light_version: prediction_textline[:,:][old_art[:,:]==1]=2 - if not self.dir_in: - prediction_textline_longshot = self.do_prediction(False, img, model_textline) - else: - prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) + prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) return ((prediction_textline[:, :, 0]==1)*1).astype('uint8'), ((prediction_textline_longshot_true_size[:, :, 0]==1)*1).astype('uint8') @@ -2161,10 +2140,8 @@ class Eynollah: if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light_only_images_extraction) - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region) - else: - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region) + self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light_only_images_extraction) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region) prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) @@ -2256,7 +2233,7 @@ class Eynollah: img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - #model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + #model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) #print(num_col_classifier,'num_col_classifier') @@ -2290,10 +2267,8 @@ class Eynollah: #img_bin = np.copy(img_resized) ###if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 30): ###if not self.dir_in: - ###model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - ###prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) - ###else: - ###prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + ###self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) + ###prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) ####print("inside bin ", time.time()-t_bin) ###prediction_bin=prediction_bin[:,:,0] @@ -2309,10 +2284,8 @@ class Eynollah: ###img_bin = np.copy(img_resized) if self.ocr and not self.input_binary: if not self.dir_in: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_resized, model_bin, n_batch_inference=5) - else: - prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) + self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) prediction_bin=prediction_bin[:,:,0] prediction_bin = (prediction_bin[:,:]==0)*1 prediction_bin = prediction_bin*255 @@ -2341,30 +2314,27 @@ class Eynollah: if not skip_layout_and_reading_order: #print("inside 2 ", time.time()-t_in) if not self.dir_in: - if num_col_classifier == 1 or num_col_classifier == 2: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) - else: - prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) - prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page + self.model_region_1_2, _ = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + ##self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light) + + if num_col_classifier == 1 or num_col_classifier == 2: + if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: + prediction_regions_org = self.do_prediction_new_concept( + True, img_resized, self.model_region_1_2, n_batch_inference=1, + thresholding_for_some_classes_in_light_version=True) else: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) - ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) + prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) + prediction_regions_page = self.do_prediction_new_concept( + False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, + thresholding_for_artificial_class_in_light_version=True) + prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: - if num_col_classifier == 1 or num_col_classifier == 2: - if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_some_classes_in_light_version=True) - else: - prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) - prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page - else: - prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), self.model_region_1_2, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) - ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) + new_h = (900+ (num_col_classifier-3)*100) + img_resized = resize_image(img_bin, int(new_h * img_bin.shape[0] /img_bin.shape[1]), new_h) + prediction_regions_org = self.do_prediction_new_concept( + True, img_resized, self.model_region_1_2, n_batch_inference=2, + thresholding_for_some_classes_in_light_version=True) + ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) #print("inside 3 ", time.time()-t_in) @@ -2466,16 +2436,13 @@ class Eynollah: img_width_h = img_org.shape[1] if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1.3 ratio_x=1 img = resize_image(img_org, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - if not self.dir_in: - prediction_regions_org_y = self.do_prediction(True, img, model_region) - else: - prediction_regions_org_y = self.do_prediction(True, img, self.model_region) + prediction_regions_org_y = self.do_prediction(True, img, self.model_region) prediction_regions_org_y = resize_image(prediction_regions_org_y, img_height_h, img_width_h ) #plt.imshow(prediction_regions_org_y[:,:,0]) @@ -2494,10 +2461,7 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1]*(1.2 if is_image_enhanced else 1))) - if self.dir_in: - prediction_regions_org = self.do_prediction(True, img, self.model_region) - else: - prediction_regions_org = self.do_prediction(True, img, model_region) + prediction_regions_org = self.do_prediction(True, img, self.model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] @@ -2505,14 +2469,11 @@ class Eynollah: if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p2) + self.model_region_p2, _ = self.start_new_session_and_model(self.model_region_dir_p2) img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1])) - if self.dir_in: - prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, marginal_of_patch_percent=0.2) - else: - prediction_regions_org2 = self.do_prediction(True, img, model_region, marginal_of_patch_percent=0.2) + prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, marginal_of_patch_percent=0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) @@ -2544,10 +2505,8 @@ class Eynollah: prediction_bin = np.copy(img_org) else: if not self.dir_in: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin, n_batch_inference=5) - else: - prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) + self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] @@ -2557,17 +2516,14 @@ class Eynollah: prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 ratio_x=1 img = resize_image(prediction_bin, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - if not self.dir_in: - prediction_regions_org = self.do_prediction(True, img, model_region) - else: - prediction_regions_org = self.do_prediction(True, img, self.model_region) + prediction_regions_org = self.do_prediction(True, img, self.model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] @@ -2597,10 +2553,8 @@ class Eynollah: prediction_bin = np.copy(img_org) if not self.dir_in: - model_bin, session_bin = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img_org, model_bin, n_batch_inference=5) - else: - prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) + self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) + prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin=prediction_bin[:,:,0] @@ -2612,7 +2566,7 @@ class Eynollah: if not self.dir_in: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens) + self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) else: prediction_bin = np.copy(img_org) @@ -2621,17 +2575,14 @@ class Eynollah: img = resize_image(prediction_bin, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) - if not self.dir_in: - prediction_regions_org = self.do_prediction(True, img, model_region) - else: - prediction_regions_org = self.do_prediction(True, img, self.model_region) + prediction_regions_org = self.do_prediction(True, img, self.model_region) prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) prediction_regions_org=prediction_regions_org[:,:,0] #mask_lines_only=(prediction_regions_org[:,:]==3)*1 #img = resize_image(img_org, int(img_org.shape[0]*1), int(img_org.shape[1]*1)) - #prediction_regions_org = self.do_prediction(True, img, model_region) + #prediction_regions_org = self.do_prediction(True, img, self.model_region) #prediction_regions_org = resize_image(prediction_regions_org, img_height_h, img_width_h ) @@ -3173,9 +3124,7 @@ class Eynollah: - if self.dir_in: - pass - else: + if not self.dir_in: self.model_table, _ = self.start_new_session_and_model(self.model_table_dir) patches = False @@ -3937,9 +3886,7 @@ class Eynollah: img_poly[text_regions_p[:,:]==3] = 4 img_poly[text_regions_p[:,:]==6] = 5 - if self.dir_in: - pass - else: + if not self.dir_in: self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) height1 =672#448 From 9f12fa241dfb09eff9119e940285e1271dfc700b Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 22:09:15 +0000 Subject: [PATCH 287/412] log-level: only set 'eynollah' logger level --- src/eynollah/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index bed0c03..5f4b5a4 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -1,6 +1,6 @@ import sys import click -from ocrd_utils import initLogging, setOverrideLogLevel +from ocrd_utils import initLogging, getLevelName, getLogger from eynollah.eynollah import Eynollah from eynollah.sbb_binarize import SbbBinarizer @@ -254,9 +254,9 @@ def binarization(patches, model_dir, input_image, output_image, dir_in, dir_out) ) def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, extract_only_images, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, skip_layout_and_reading_order, ignore_page_extraction, log_level): - if log_level: - setOverrideLogLevel(log_level) initLogging() + if log_level: + getLogger('eynollah').setLevel(getLevelName(log_level)) if not enable_plotting and (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): print("Error: You used one of -sl, -sd, -sa, -sp, -si or -ae but did not enable plotting with -ep") sys.exit(1) From 5b82320707e92162220b819734705c40e77bce74 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 22:09:32 +0000 Subject: [PATCH 288/412] avoid indentation --- src/eynollah/eynollah.py | 899 ++++++++++++++++++++------------------- 1 file changed, 450 insertions(+), 449 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index d11531a..4f1d8e3 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4926,496 +4926,497 @@ class Eynollah: if self.dir_in: self.writer.write_pagexml(pcgts) + continue else: return pcgts - else: - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) - #print("text region early -1 in %.1fs", time.time() - t0) - t1 = time.time() - if not self.skip_layout_and_reading_order: - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) - - if num_col_classifier == 1 or num_col_classifier ==2: - if num_col_classifier == 1: - img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) - else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - #print("text region early in %.1fs", time.time() - t0) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) + #print("text region early -1 in %.1fs", time.time() - t0) + t1 = time.time() + if not self.skip_layout_and_reading_order: + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) - t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() - if self.light_version and num_col_classifier in (1,2): - org_h_l_m = textline_mask_tot_ea.shape[0] - org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: - img_w_new = 2000 + img_w_new = 1000 img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - + elif num_col_classifier == 2: - img_w_new = 2400 + img_w_new = 1300 img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - image_page = resize_image(image_page,img_h_new, img_w_new ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - mask_images = resize_image(mask_images,img_h_new, img_w_new ) - mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) - text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) - table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) - - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - - if self.light_version and num_col_classifier in (1,2): - image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) - text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) - textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) - text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) - table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) - image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) - - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) - ## birdan sora chock chakir + + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + else: + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - if self.full_layout: - if not self.light_version: - img_bin_light = None - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - - if self.light_version: - drop_label_in_full_layout = 4 - textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 - - - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - - #print("text region early 2 in %.1fs", time.time() - t0) - ###min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + #print("text region early in %.1fs", time.time() - t0) + t1 = time.time() + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + t1 = time.time() + #plt.imshow(table_prediction) + #plt.show() + if self.light_version and num_col_classifier in (1,2): + org_h_l_m = textline_mask_tot_ea.shape[0] + org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1: + img_w_new = 2000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + elif num_col_classifier == 2: + img_w_new = 2400 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) - #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - - #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + image_page = resize_image(image_page,img_h_new, img_w_new ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + mask_images = resize_image(mask_images,img_h_new, img_w_new ) + mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) + text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) + table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) - - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big + if self.light_version and num_col_classifier in (1,2): + image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) + text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) + textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) + text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) + table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) + image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] - + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) + ## birdan sora chock chakir + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + if self.full_layout: + if not self.light_version: + img_bin_light = None + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + + if self.light_version: + drop_label_in_full_layout = 4 + textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 + + + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + #print("text region early 2 in %.1fs", time.time() - t0) + ###min_con_area = 0.000005 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] + else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + else: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - #try: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - #except: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass - - #print("text region early 3 in %.1fs", time.time() - t0) + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + #try: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + #except: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) + # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) + else: + pass + + #print("text region early 3 in %.1fs", time.time() - t0) + if self.light_version: + contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + #print("text region early 3.5 in %.1fs", time.time() - t0) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) + #txt_con_org = self.dilate_textregions_contours(txt_con_org) + #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) + #print("text region early 5 in %.1fs", time.time() - t0) + ## birdan sora chock chakir + if not self.curved_line: if self.light_version: - contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) - #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) - #txt_con_org = self.dilate_textregions_contours(txt_con_org) - #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) - ## birdan sora chock chakir - if not self.curved_line: - if self.light_version: - if self.textline_light: - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - - #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) - #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) - #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) - - contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) - - else: - textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + if self.textline_light: + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + + contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) + else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: - - scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + else: + + scale_param = 1 + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + #print("text region early 6 in %.1fs", time.time() - t0) + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - - if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) - pixel_lines = 6 - - if not self.reading_order_machine_based: - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if not self.reading_order_machine_based: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() - - if self.full_layout: - - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) + + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) + pixel_lines = 6 + + if not self.reading_order_machine_based: + if not self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - - if self.ocr: - ocr_all_textlines = [] + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + + if not self.reading_order_machine_based: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) else: - ocr_all_textlines = None - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - - + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + + if self.full_layout: + + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: - contours_only_text_parent_h = None - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - if self.ocr: + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None - device = cuda.get_current_device() - device.reset() - gc.collect() - model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") - torch.cuda.empty_cache() - model_ocr.to(device) - - ind_tot = 0 - #cv2.imwrite('./img_out.png', image_page) - - ocr_all_textlines = [] - for indexing, ind_poly_first in enumerate(all_found_textline_polygons): - ocr_textline_in_textregion = [] - for indexing2, ind_poly in enumerate(ind_poly_first): - if not (self.textline_light or self.curved_line): - ind_poly = copy.deepcopy(ind_poly) - box_ind = all_box_coord[indexing] - #print(ind_poly,np.shape(ind_poly), 'ind_poly') - #print(box_ind) - ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) - #print(ind_poly_copy) - ind_poly[ind_poly<0] = 0 - x, y, w, h = cv2.boundingRect(ind_poly) - #print(ind_poly_copy, np.shape(ind_poly_copy)) - #print(x, y, w, h, h/float(w),'ratio') - h2w_ratio = h/float(w) - mask_poly = np.zeros(image_page.shape) - if not self.light_version: - img_poly_on_img = np.copy(image_page) - else: - img_poly_on_img = np.copy(img_bin_light) - - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) - - if self.textline_light: - mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 - - img_croped = img_poly_on_img[y:y+h, x:x+w, :] - #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) - text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) - - ocr_textline_in_textregion.append(text_ocr) - - - ind_tot = ind_tot +1 - ocr_all_textlines.append(ocr_textline_in_textregion) - - else: - ocr_all_textlines = None - #print(ocr_all_textlines) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) - else: - _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) - - page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) - - - ##all_found_textline_polygons =self.scale_contours_new(textline_mask_tot_ea) - - cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) - all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) - - all_found_textline_polygons=[ all_found_textline_polygons ] - - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") - - - order_text_new = [0] - slopes =[0] - id_of_texts_tot =['region_0001'] - - polygons_of_images = [] - slopes_marginals = [] - polygons_of_marginals = [] - all_found_textline_polygons_marginals = [] - all_box_coord_marginals = [] - polygons_lines_xml = [] - contours_tables = [] - ocr_all_textlines = None - - pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts - - if self.dir_in: - self.writer.write_pagexml(pcgts) - #self.logger.info("Job done in %.1fs", time.time() - t0) - print("Job done in %.1fs", time.time() - t0) + + + else: + contours_only_text_parent_h = None + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) + else: + _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) + + page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) + + + ##all_found_textline_polygons =self.scale_contours_new(textline_mask_tot_ea) + + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) + all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + + all_found_textline_polygons=[ all_found_textline_polygons ] + + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") + + + order_text_new = [0] + slopes =[0] + id_of_texts_tot =['region_0001'] + + polygons_of_images = [] + slopes_marginals = [] + polygons_of_marginals = [] + all_found_textline_polygons_marginals = [] + all_box_coord_marginals = [] + polygons_lines_xml = [] + contours_tables = [] + ocr_all_textlines = None + + pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + if not self.dir_in: + return pcgts + + if self.dir_in: + self.writer.write_pagexml(pcgts) + #self.logger.info("Job done in %.1fs", time.time() - t0) + print("Job done in %.1fs" % time.time() - t0) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From cd4e426977193e34452f76abf5af8b7af222d8b0 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 22:11:34 +0000 Subject: [PATCH 289/412] avoid indentation (skip_layout_and_reading_order) --- src/eynollah/eynollah.py | 896 ++++++++++++++++++++------------------- 1 file changed, 449 insertions(+), 447 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 4f1d8e3..2772bd4 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4934,452 +4934,7 @@ class Eynollah: self.logger.info("Enhancing took %.1fs ", time.time() - t0) #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() - if not self.skip_layout_and_reading_order: - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) - - if num_col_classifier == 1 or num_col_classifier ==2: - if num_col_classifier == 1: - img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) - else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - - t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - #print("text region early in %.1fs", time.time() - t0) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) - - t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() - if self.light_version and num_col_classifier in (1,2): - org_h_l_m = textline_mask_tot_ea.shape[0] - org_w_l_m = textline_mask_tot_ea.shape[1] - if num_col_classifier == 1: - img_w_new = 2000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 2400 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - image_page = resize_image(image_page,img_h_new, img_w_new ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - mask_images = resize_image(mask_images,img_h_new, img_w_new ) - mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) - text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) - table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) - - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - - if self.light_version and num_col_classifier in (1,2): - image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) - text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) - textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) - text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) - table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) - image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) - - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) - ## birdan sora chock chakir - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - if self.full_layout: - if not self.light_version: - img_bin_light = None - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - - if self.light_version: - drop_label_in_full_layout = 4 - textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 - - - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - - #print("text region early 2 in %.1fs", time.time() - t0) - ###min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) - #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - - #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) - - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) - - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) - - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big - - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] - - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] - else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - #try: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - #except: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass - - #print("text region early 3 in %.1fs", time.time() - t0) - if self.light_version: - contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) - #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) - #txt_con_org = self.dilate_textregions_contours(txt_con_org) - #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) - ## birdan sora chock chakir - if not self.curved_line: - if self.light_version: - if self.textline_light: - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - - #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) - #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) - #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) - - contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) - - else: - textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - else: - textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - else: - - scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - - if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) - pixel_lines = 6 - - if not self.reading_order_machine_based: - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if not self.reading_order_machine_based: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - - if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() - - if self.full_layout: - - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - - if self.ocr: - ocr_all_textlines = [] - else: - ocr_all_textlines = None - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - - - else: - contours_only_text_parent_h = None - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - - - if self.ocr: - - device = cuda.get_current_device() - device.reset() - gc.collect() - model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") - torch.cuda.empty_cache() - model_ocr.to(device) - - ind_tot = 0 - #cv2.imwrite('./img_out.png', image_page) - - ocr_all_textlines = [] - for indexing, ind_poly_first in enumerate(all_found_textline_polygons): - ocr_textline_in_textregion = [] - for indexing2, ind_poly in enumerate(ind_poly_first): - if not (self.textline_light or self.curved_line): - ind_poly = copy.deepcopy(ind_poly) - box_ind = all_box_coord[indexing] - #print(ind_poly,np.shape(ind_poly), 'ind_poly') - #print(box_ind) - ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) - #print(ind_poly_copy) - ind_poly[ind_poly<0] = 0 - x, y, w, h = cv2.boundingRect(ind_poly) - #print(ind_poly_copy, np.shape(ind_poly_copy)) - #print(x, y, w, h, h/float(w),'ratio') - h2w_ratio = h/float(w) - mask_poly = np.zeros(image_page.shape) - if not self.light_version: - img_poly_on_img = np.copy(image_page) - else: - img_poly_on_img = np.copy(img_bin_light) - - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) - - if self.textline_light: - mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 - - img_croped = img_poly_on_img[y:y+h, x:x+w, :] - #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) - text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) - - ocr_textline_in_textregion.append(text_ocr) - - - ind_tot = ind_tot +1 - ocr_all_textlines.append(ocr_textline_in_textregion) - - else: - ocr_all_textlines = None - #print(ocr_all_textlines) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) - else: + if self.skip_layout_and_reading_order: _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) @@ -5410,13 +4965,460 @@ class Eynollah: ocr_all_textlines = None pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + if self.dir_in: + continue + else: + return pcgts + + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) + + if num_col_classifier == 1 or num_col_classifier ==2: + if num_col_classifier == 1: + img_w_new = 1000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 1300 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + else: + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) + + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + #print("text region early in %.1fs", time.time() - t0) + t1 = time.time() + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) + + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + t1 = time.time() + #plt.imshow(table_prediction) + #plt.show() + if self.light_version and num_col_classifier in (1,2): + org_h_l_m = textline_mask_tot_ea.shape[0] + org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1: + img_w_new = 2000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 2400 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + image_page = resize_image(image_page,img_h_new, img_w_new ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + mask_images = resize_image(mask_images,img_h_new, img_w_new ) + mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) + text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) + table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) + + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + + if self.light_version and num_col_classifier in (1,2): + image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) + text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) + textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) + text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) + table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) + image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) + + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) + ## birdan sora chock chakir + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + if self.full_layout: + if not self.light_version: + img_bin_light = None + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + + if self.light_version: + drop_label_in_full_layout = 4 + textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 + + + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + #print("text region early 2 in %.1fs", time.time() - t0) + ###min_con_area = 0.000005 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] + else: + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + #try: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + #except: + #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) + # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) + # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) + else: + pass + + #print("text region early 3 in %.1fs", time.time() - t0) + if self.light_version: + contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + #print("text region early 3.5 in %.1fs", time.time() - t0) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) + #txt_con_org = self.dilate_textregions_contours(txt_con_org) + #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) + #print("text region early 5 in %.1fs", time.time() - t0) + ## birdan sora chock chakir + if not self.curved_line: + if self.light_version: + if self.textline_light: + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + + contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) + + else: + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + else: + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + else: + + scale_param = 1 + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + #print("text region early 6 in %.1fs", time.time() - t0) + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + else: + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + + if self.plotter: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) + + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) + pixel_lines = 6 + + if not self.reading_order_machine_based: + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + + if not self.reading_order_machine_based: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + + if self.full_layout: + + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None + + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts + + else: + contours_only_text_parent_h = None + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) + if self.dir_in: self.writer.write_pagexml(pcgts) #self.logger.info("Job done in %.1fs", time.time() - t0) - print("Job done in %.1fs" % time.time() - t0) + print("Job done in %.1fs" % (time.time() - t0)) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From a520bd1f771b9263ed865e879248b38692cdef86 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 4 Dec 2024 22:49:34 +0000 Subject: [PATCH 290/412] wrap extremely long lines --- src/eynollah/eynollah.py | 95 ++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 2772bd4..769093d 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4935,7 +4935,8 @@ class Eynollah: #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() if self.skip_layout_and_reading_order: - _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) + _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, + skip_layout_and_reading_order=self.skip_layout_and_reading_order) page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) @@ -4964,7 +4965,10 @@ class Eynollah: contours_tables = [] ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) if self.dir_in: continue else: @@ -5005,6 +5009,8 @@ class Eynollah: self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) self.logger.info("Graphics detection took %.1fs ", time.time() - t1) #self.logger.info('cont_page %s', cont_page) + #plt.imshow(table_prediction) + #plt.show() if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") @@ -5016,19 +5022,16 @@ class Eynollah: continue else: return pcgts + #print("text region early in %.1fs", time.time() - t0) t1 = time.time() if not self.light_version: textline_mask_tot_ea = self.run_textline(image_page) self.logger.info("textline detection took %.1fs", time.time() - t1) - t1 = time.time() slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) self.logger.info("deskewing took %.1fs", time.time() - t1) - t1 = time.time() - #plt.imshow(table_prediction) - #plt.show() - if self.light_version and num_col_classifier in (1,2): + elif num_col_classifier in (1,2): org_h_l_m = textline_mask_tot_ea.shape[0] org_w_l_m = textline_mask_tot_ea.shape[1] if num_col_classifier == 1: @@ -5062,14 +5065,13 @@ class Eynollah: ## birdan sora chock chakir t1 = time.time() if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ + self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - if self.full_layout: - if not self.light_version: - img_bin_light = None - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light) + else: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ + self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - if self.light_version: drop_label_in_full_layout = 4 textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 @@ -5219,14 +5221,19 @@ class Eynollah: if not self.curved_line: if self.light_version: if self.textline_light: - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ + # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ + # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) @@ -5237,22 +5244,28 @@ class Eynollah: else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - + slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: - scale_param = 1 - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2), image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ + self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: @@ -5261,17 +5274,17 @@ class Eynollah: #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) #except: #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) else: #takes long timee contours_only_text_parent_d_ordered = None - if self.light_version: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header_light(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - else: - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = check_any_text_region_in_model_one_is_main_or_header(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + if self.light_version: + fun = check_any_text_region_in_model_one_is_main_or_header_light + else: + fun = check_any_text_region_in_model_one_is_main_or_header + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ + all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ + contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ + fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) if self.plotter: self.plotter.save_plot_of_layout(text_regions_p, image_page) @@ -5279,7 +5292,9 @@ class Eynollah: pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, + all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, + kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) pixel_lines = 6 if not self.reading_order_machine_based: @@ -5303,7 +5318,6 @@ class Eynollah: regions_without_separators_d = regions_without_separators_d.astype(np.uint8) regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - if not self.reading_order_machine_based: if np.abs(slope_deskew) < SLOPE_THRESHOLD: boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) else: @@ -5329,7 +5343,10 @@ class Eynollah: else: ocr_all_textlines = None - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, + cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts @@ -5409,7 +5426,9 @@ class Eynollah: ocr_all_textlines = None #print(ocr_all_textlines) self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts From 3d88b207fc15c73b44675eb9e454840531095825 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 5 Dec 2024 09:39:55 +0000 Subject: [PATCH 291/412] run: log instead of print --- src/eynollah/eynollah.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 769093d..6333a7f 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4906,7 +4906,7 @@ class Eynollah: self.ls_imgs = [1] for img_name in self.ls_imgs: - print(img_name) + self.logger.info(img_name) t0 = time.time() if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) @@ -5436,8 +5436,8 @@ class Eynollah: if self.dir_in: self.writer.write_pagexml(pcgts) - #self.logger.info("Job done in %.1fs", time.time() - t0) - print("Job done in %.1fs" % (time.time() - t0)) + self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs" % (time.time() - t0)) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From aaea2ef4637b19a2731119f763085ee1eda12249 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 5 Dec 2024 09:40:02 +0000 Subject: [PATCH 292/412] simplify --- src/eynollah/eynollah.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 6333a7f..a3e6f9e 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -832,8 +832,7 @@ class Eynollah: img = img / float(255.0) img = resize_image(img, img_height_model, img_width_model) - label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), - verbose=0) + label_p_pred = model.predict(img[np.newaxis], verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] @@ -1082,6 +1081,7 @@ class Eynollah: #del model #gc.collect() return prediction_true + def do_padding_with_scale(self,img, scale): h_n = int(img.shape[0]*scale) w_n = int(img.shape[1]*scale) @@ -2032,22 +2032,20 @@ class Eynollah: all_box_coord_per_process.append(crop_coor) queue_of_all_params.put([slopes_per_each_subprocess, textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours]) - def textline_contours(self, img, patches, scaler_h, scaler_w, num_col_classifier=None): + def textline_contours(self, img, use_patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') - if self.textline_light: - thresholding_for_artificial_class_in_light_version = True#False - else: - thresholding_for_artificial_class_in_light_version = False if not self.dir_in: self.model_textline, _ = self.start_new_session_and_model(self.model_textline_dir) + #img = img.astype(np.uint8) img_org = np.copy(img) img_h = img_org.shape[0] img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - #if not thresholding_for_artificial_class_in_light_version: + prediction_textline = self.do_prediction(use_patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, + thresholding_for_artificial_class_in_light_version=self.textline_light) + #if not self.textline_light: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, self.model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 @@ -2057,7 +2055,7 @@ class Eynollah: old_art = np.copy(textline_mask_tot_ea_art) - if not thresholding_for_artificial_class_in_light_version: + if not self.textline_light: textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') #textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) @@ -2066,12 +2064,12 @@ class Eynollah: textline_mask_tot_ea_lines = (prediction_textline[:,:]==1)*1 textline_mask_tot_ea_lines = textline_mask_tot_ea_lines.astype('uint8') - if not thresholding_for_artificial_class_in_light_version: + if not self.textline_light: textline_mask_tot_ea_lines = cv2.dilate(textline_mask_tot_ea_lines, KERNEL, iterations=1) prediction_textline[:,:][textline_mask_tot_ea_lines[:,:]==1]=1 - if not thresholding_for_artificial_class_in_light_version: + if not self.textline_light: prediction_textline[:,:][old_art[:,:]==1]=2 prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) @@ -3366,8 +3364,7 @@ class Eynollah: scaler_h_textline = 1#1.3 # 1.2#1.2 scaler_w_textline = 1#1.3 # 0.9#1 #print(image_page.shape) - patches = True - textline_mask_tot_ea, _ = self.textline_contours(image_page, patches, scaler_h_textline, scaler_w_textline, num_col_classifier) + textline_mask_tot_ea, _ = self.textline_contours(image_page, True, scaler_h_textline, scaler_w_textline, num_col_classifier) if self.textline_light: textline_mask_tot_ea = textline_mask_tot_ea.astype(np.int16) From 055463d23a3ef6b3bbdf2581740c5d0dab3d501a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 5 Dec 2024 09:43:30 +0000 Subject: [PATCH 293/412] avoid indentation --- src/eynollah/eynollah.py | 453 +++++++++++++++++++-------------------- 1 file changed, 226 insertions(+), 227 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index a3e6f9e..4cf9e81 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -846,238 +846,237 @@ class Eynollah: seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) prediction_true = prediction_true.astype(np.uint8) + return prediction_true + + if img.shape[0] < img_height_model: + img = resize_image(img, img_height_model, img.shape[1]) + + if img.shape[1] < img_width_model: + img = resize_image(img, img.shape[0], img_width_model) + + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) + margin = int(marginal_of_patch_percent * img_height_model) + width_mid = img_width_model - 2 * margin + height_mid = img_height_model - 2 * margin + img = img / float(255.0) + #img = img.astype(np.float16) + img_h = img.shape[0] + img_w = img.shape[1] + prediction_true = np.zeros((img_h, img_w, 3)) + mask_true = np.zeros((img_h, img_w)) + nxf = img_w / float(width_mid) + nyf = img_h / float(height_mid) + nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) + nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + for i in range(nxf): + for j in range(nyf): + if i == 0: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + else: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + if j == 0: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + else: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + if index_x_u > img_w: + index_x_u = img_w + index_x_d = img_w - img_width_model + if index_y_u > img_h: + index_y_u = img_h + index_y_d = img_h - img_height_model + + list_i_s.append(i) + list_j_s.append(j) + list_x_u.append(index_x_u) + list_x_d.append(index_x_d) + list_y_d.append(index_y_d) + list_y_u.append(index_y_u) - else: - if img.shape[0] < img_height_model: - img = resize_image(img, img_height_model, img.shape[1]) + img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - if img.shape[1] < img_width_model: - img = resize_image(img, img.shape[0], img_width_model) + batch_indexer = batch_indexer + 1 - self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) - margin = int(marginal_of_patch_percent * img_height_model) - width_mid = img_width_model - 2 * margin - height_mid = img_height_model - 2 * margin - img = img / float(255.0) - #img = img.astype(np.float16) - img_h = img.shape[0] - img_w = img.shape[1] - prediction_true = np.zeros((img_h, img_w, 3)) - mask_true = np.zeros((img_h, img_w)) - nxf = img_w / float(width_mid) - nyf = img_h / float(height_mid) - nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) - nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - for i in range(nxf): - for j in range(nyf): - if i == 0: - index_x_d = i * width_mid - index_x_u = index_x_d + img_width_model - else: - index_x_d = i * width_mid - index_x_u = index_x_d + img_width_model - if j == 0: - index_y_d = j * height_mid - index_y_u = index_y_d + img_height_model - else: - index_y_d = j * height_mid - index_y_u = index_y_d + img_height_model - if index_x_u > img_w: - index_x_u = img_w - index_x_d = img_w - img_width_model - if index_y_u > img_h: - index_y_u = img_h - index_y_d = img_h - img_height_model - - list_i_s.append(i) - list_j_s.append(j) - list_x_u.append(index_x_u) - list_x_d.append(index_x_d) - list_y_d.append(index_y_d) - list_y_u.append(index_y_u) - + if batch_indexer == n_batch_inference: + label_p_pred = model.predict(img_patch,verbose=0) - img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - - batch_indexer = batch_indexer + 1 - - if batch_indexer == n_batch_inference: - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - - if thresholding_for_some_classes_in_light_version: - seg_not_base = label_p_pred[:,:,:,4] - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg_background = label_p_pred[:,:,:,0] - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 - - seg[seg_not_base==1]=4 - seg[seg_background==1]=0 - seg[(seg_line==1) & (seg==0)]=3 - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - - elif i==(nxf-1) and j==(nyf-1): - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - if thresholding_for_some_classes_in_light_version: - seg_not_base = label_p_pred[:,:,:,4] - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg_background = label_p_pred[:,:,:,0] - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 - - seg[seg_not_base==1]=4 - seg[seg_background==1]=0 - seg[(seg_line==1) & (seg==0)]=3 - - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - - prediction_true = prediction_true.astype(np.uint8) + seg = np.argmax(label_p_pred, axis=3) + + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + elif i==(nxf-1) and j==(nyf-1): + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + if thresholding_for_some_classes_in_light_version: + seg_not_base = label_p_pred[:,:,:,4] + seg_not_base[seg_not_base>0.03] =1 + seg_not_base[seg_not_base<1] =0 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg_background = label_p_pred[:,:,:,0] + seg_background[seg_background>0.25] =1 + seg_background[seg_background<1] =0 + + seg[seg_not_base==1]=4 + seg[seg_background==1]=0 + seg[(seg_line==1) & (seg==0)]=3 + + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + prediction_true = prediction_true.astype(np.uint8) #del model #gc.collect() return prediction_true From c3163caefdb9a0843cc3e3f1408ff874fc4ce46e Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Thu, 5 Dec 2024 14:28:17 +0000 Subject: [PATCH 294/412] avoid indentation --- src/eynollah/eynollah.py | 425 +++++++++++++++++++-------------------- 1 file changed, 212 insertions(+), 213 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 4cf9e81..d483cac 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1140,227 +1140,226 @@ class Eynollah: seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) prediction_true = resize_image(seg_color, img_h_page, img_w_page) prediction_true = prediction_true.astype(np.uint8) + return prediction_true + + if img.shape[0] < img_height_model: + img = resize_image(img, img_height_model, img.shape[1]) + + if img.shape[1] < img_width_model: + img = resize_image(img, img.shape[0], img_width_model) + + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) + margin = int(marginal_of_patch_percent * img_height_model) + width_mid = img_width_model - 2 * margin + height_mid = img_height_model - 2 * margin + img = img / float(255.0) + img = img.astype(np.float16) + img_h = img.shape[0] + img_w = img.shape[1] + prediction_true = np.zeros((img_h, img_w, 3)) + mask_true = np.zeros((img_h, img_w)) + nxf = img_w / float(width_mid) + nyf = img_h / float(height_mid) + nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) + nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + for i in range(nxf): + for j in range(nyf): + if i == 0: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + else: + index_x_d = i * width_mid + index_x_u = index_x_d + img_width_model + if j == 0: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + else: + index_y_d = j * height_mid + index_y_u = index_y_d + img_height_model + if index_x_u > img_w: + index_x_u = img_w + index_x_d = img_w - img_width_model + if index_y_u > img_h: + index_y_u = img_h + index_y_d = img_h - img_height_model - else: - if img.shape[0] < img_height_model: - img = resize_image(img, img_height_model, img.shape[1]) + list_i_s.append(i) + list_j_s.append(j) + list_x_u.append(index_x_u) + list_x_d.append(index_x_d) + list_y_d.append(index_y_d) + list_y_u.append(index_y_u) - if img.shape[1] < img_width_model: - img = resize_image(img, img.shape[0], img_width_model) - self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) - margin = int(marginal_of_patch_percent * img_height_model) - width_mid = img_width_model - 2 * margin - height_mid = img_height_model - 2 * margin - img = img / float(255.0) - img = img.astype(np.float16) - img_h = img.shape[0] - img_w = img.shape[1] - prediction_true = np.zeros((img_h, img_w, 3)) - mask_true = np.zeros((img_h, img_w)) - nxf = img_w / float(width_mid) - nyf = img_h / float(height_mid) - nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) - nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - for i in range(nxf): - for j in range(nyf): - if i == 0: - index_x_d = i * width_mid - index_x_u = index_x_d + img_width_model - else: - index_x_d = i * width_mid - index_x_u = index_x_d + img_width_model - if j == 0: - index_y_d = j * height_mid - index_y_u = index_y_d + img_height_model - else: - index_y_d = j * height_mid - index_y_u = index_y_d + img_height_model - if index_x_u > img_w: - index_x_u = img_w - index_x_d = img_w - img_width_model - if index_y_u > img_h: - index_y_u = img_h - index_y_d = img_h - img_height_model - - - list_i_s.append(i) - list_j_s.append(j) - list_x_u.append(index_x_u) - list_x_d.append(index_x_d) - list_y_d.append(index_y_d) - list_y_u.append(index_y_u) - + batch_indexer = batch_indexer + 1 - img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - - batch_indexer = batch_indexer + 1 + if batch_indexer == n_batch_inference: + label_p_pred = model.predict(img_patch,verbose=0) - if batch_indexer == n_batch_inference: - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - - if thresholding_for_some_classes_in_light_version: - seg_art = label_p_pred[:,:,:,4] - seg_art[seg_art<0.2] =0 - seg_art[seg_art>0] =1 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg[seg_art==1]=4 - seg[(seg_line==1) & (seg==0)]=3 - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - - elif i==(nxf-1) and j==(nyf-1): - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - if thresholding_for_some_classes_in_light_version: - seg_art = label_p_pred[:,:,:,4] - seg_art[seg_art<0.2] =0 - seg_art[seg_art>0] =1 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg[seg_art==1]=4 - seg[(seg_line==1) & (seg==0)]=3 - - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + seg = np.argmax(label_p_pred, axis=3) - prediction_true = prediction_true.astype(np.uint8) + if thresholding_for_some_classes_in_light_version: + seg_art = label_p_pred[:,:,:,4] + seg_art[seg_art<0.2] =0 + seg_art[seg_art>0] =1 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg[seg_art==1]=4 + seg[(seg_line==1) & (seg==0)]=3 + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + elif i==(nxf-1) and j==(nyf-1): + label_p_pred = model.predict(img_patch,verbose=0) + + seg = np.argmax(label_p_pred, axis=3) + if thresholding_for_some_classes_in_light_version: + seg_art = label_p_pred[:,:,:,4] + seg_art[seg_art<0.2] =0 + seg_art[seg_art>0] =1 + + seg_line = label_p_pred[:,:,:,3] + seg_line[seg_line>0.1] =1 + seg_line[seg_line<1] =0 + + seg[seg_art==1]=4 + seg[(seg_line==1) & (seg==0)]=3 + + if thresholding_for_artificial_class_in_light_version: + seg_art = label_p_pred[:,:,:,2] + + seg_art[seg_art<0.2] = 0 + seg_art[seg_art>0] =1 + + seg[seg_art==1]=2 + + indexer_inside_batch = 0 + for i_batch, j_batch in zip(list_i_s, list_j_s): + seg_in = seg[indexer_inside_batch,:,:] + seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + + index_y_u_in = list_y_u[indexer_inside_batch] + index_y_d_in = list_y_d[indexer_inside_batch] + + index_x_u_in = list_x_u[indexer_inside_batch] + index_x_d_in = list_x_d[indexer_inside_batch] + + if i_batch == 0 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: + seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: + seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + else: + seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] + prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + + indexer_inside_batch = indexer_inside_batch +1 + + list_i_s = [] + list_j_s = [] + list_x_u = [] + list_x_d = [] + list_y_u = [] + list_y_d = [] + + batch_indexer = 0 + img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + + prediction_true = prediction_true.astype(np.uint8) return prediction_true def extract_page(self): From ad748d003978b643dab6ec482542b03fdb3dc1e4 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 9 Dec 2024 10:55:41 +0000 Subject: [PATCH 295/412] do_prediction: avoid code duplication --- src/eynollah/eynollah.py | 169 +++------------------------------------ 1 file changed, 9 insertions(+), 160 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index d483cac..50f0f34 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -912,7 +912,10 @@ class Eynollah: batch_indexer = batch_indexer + 1 - if batch_indexer == n_batch_inference: + if (batch_indexer == n_batch_inference or + # last batch + i == nxf - 1 and j == nyf - 1): + self.logger.debug("predicting patches on %s", str(img_patch.shape)) label_p_pred = model.predict(img_patch,verbose=0) seg = np.argmax(label_p_pred, axis=3) @@ -994,88 +997,6 @@ class Eynollah: img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - elif i==(nxf-1) and j==(nyf-1): - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - if thresholding_for_some_classes_in_light_version: - seg_not_base = label_p_pred[:,:,:,4] - seg_not_base[seg_not_base>0.03] =1 - seg_not_base[seg_not_base<1] =0 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg_background = label_p_pred[:,:,:,0] - seg_background[seg_background>0.25] =1 - seg_background[seg_background<1] =0 - - seg[seg_not_base==1]=4 - seg[seg_background==1]=0 - seg[(seg_line==1) & (seg==0)]=3 - - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - prediction_true = prediction_true.astype(np.uint8) #del model #gc.collect() @@ -1111,7 +1032,7 @@ class Eynollah: return img_scaled_padded#, label_scaled_padded def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): - self.logger.debug("enter do_prediction") + self.logger.debug("enter do_prediction_new_concept") img_height_model = model.layers[len(model.layers) - 1].output_shape[1] img_width_model = model.layers[len(model.layers) - 1].output_shape[2] @@ -1207,7 +1128,10 @@ class Eynollah: batch_indexer = batch_indexer + 1 - if batch_indexer == n_batch_inference: + if (batch_indexer == n_batch_inference or + # last batch + i == nxf - 1 and j == nyf - 1): + self.logger.debug("predicting patches on %s", str(img_patch.shape)) label_p_pred = model.predict(img_patch,verbose=0) seg = np.argmax(label_p_pred, axis=3) @@ -1284,81 +1208,6 @@ class Eynollah: img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - elif i==(nxf-1) and j==(nyf-1): - label_p_pred = model.predict(img_patch,verbose=0) - - seg = np.argmax(label_p_pred, axis=3) - if thresholding_for_some_classes_in_light_version: - seg_art = label_p_pred[:,:,:,4] - seg_art[seg_art<0.2] =0 - seg_art[seg_art>0] =1 - - seg_line = label_p_pred[:,:,:,3] - seg_line[seg_line>0.1] =1 - seg_line[seg_line<1] =0 - - seg[seg_art==1]=4 - seg[(seg_line==1) & (seg==0)]=3 - - if thresholding_for_artificial_class_in_light_version: - seg_art = label_p_pred[:,:,:,2] - - seg_art[seg_art<0.2] = 0 - seg_art[seg_art>0] =1 - - seg[seg_art==1]=2 - - indexer_inside_batch = 0 - for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) - - index_y_u_in = list_y_u[indexer_inside_batch] - index_y_d_in = list_y_d[indexer_inside_batch] - - index_x_u_in = list_x_u[indexer_inside_batch] - index_x_d_in = list_x_d[indexer_inside_batch] - - if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color - elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - - list_i_s = [] - list_j_s = [] - list_x_u = [] - list_x_d = [] - list_y_u = [] - list_y_d = [] - - batch_indexer = 0 - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - prediction_true = prediction_true.astype(np.uint8) return prediction_true From d68017037ce0f42dc036b30e1de36d8c49b73429 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 9 Dec 2024 11:27:11 +0000 Subject: [PATCH 296/412] do_prediction: trigger GC to avoid CUDA OOM --- src/eynollah/eynollah.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 50f0f34..90824c8 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -999,7 +999,7 @@ class Eynollah: prediction_true = prediction_true.astype(np.uint8) #del model - #gc.collect() + gc.collect() return prediction_true def do_padding_with_scale(self,img, scale): @@ -1209,6 +1209,7 @@ class Eynollah: img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) prediction_true = prediction_true.astype(np.uint8) + gc.collect() return prediction_true def extract_page(self): From 6fe02df97394edcc741243a9f3d635e4b2b472e2 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 9 Dec 2024 16:35:31 +0000 Subject: [PATCH 297/412] do_image_rotation: fix f93fa12 (do return results) --- src/eynollah/utils/separate_lines.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index f8df33f..a57acbb 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1573,13 +1573,14 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): def do_image_rotation(queue_of_all_params,angels_per_process, img_resized, sigma_des): angels_per_each_subprocess = [] for mv in range(len(angels_per_process)): + print(f"rotating image by {angels_per_process[mv]}") img_rot=rotate_image(img_resized,angels_per_process[mv]) img_rot[img_rot!=0]=1 try: var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) except: var_spectrum=0 - angels_per_each_subprocess.append(var_spectrum) + angels_per_each_subprocess.append(var_spectrum) queue_of_all_params.put([angels_per_each_subprocess]) From 54cb15056b352fa8f04212071851f4e66b5931bb Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 9 Dec 2024 16:37:34 +0000 Subject: [PATCH 298/412] do_image_rotation / return_deskew_slop: avoid code duplication, simplify via mp.Pool --- src/eynollah/utils/separate_lines.py | 418 +++------------------------ 1 file changed, 45 insertions(+), 373 deletions(-) diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index a57acbb..36a1b01 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1,3 +1,4 @@ +from functools import partial import numpy as np import cv2 from scipy.signal import find_peaks @@ -1570,19 +1571,15 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): # plt.show() return img_patch_ineterst_revised -def do_image_rotation(queue_of_all_params,angels_per_process, img_resized, sigma_des): - angels_per_each_subprocess = [] - for mv in range(len(angels_per_process)): - print(f"rotating image by {angels_per_process[mv]}") - img_rot=rotate_image(img_resized,angels_per_process[mv]) - img_rot[img_rot!=0]=1 - try: - var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - except: - var_spectrum=0 - angels_per_each_subprocess.append(var_spectrum) - - queue_of_all_params.put([angels_per_each_subprocess]) +def do_image_rotation(angle, img, sigma_des): + print(f"rotating image by {angle}") + img_rot = rotate_image(img, angle) + img_rot[img_rot!=0] = 1 + try: + var = find_num_col_deskew(img_rot, sigma_des, 20.3) + except: + var = 0 + return var def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=False, plotter=None): num_cores = cpu_count() @@ -1613,376 +1610,51 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals #plt.imshow(img_resized) #plt.show() - if main_page and img_patch_org.shape[1]>img_patch_org.shape[0]: - + if main_page and img_patch_org.shape[1] > img_patch_org.shape[0]: #plt.imshow(img_resized) #plt.show() - angels=np.array([-45, 0 , 45 , 90 , ])#np.linspace(-12,12,100)#np.array([0 , 45 , 90 , -45]) + angles = np.array([-45, 0, 45, 90,]) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + + angles = np.linspace(angle - 22.5, angle + 22.5, n_tot_angles) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - ###for rot in angels: - ###img_rot=rotate_image(img_resized,rot) - ####plt.imshow(img_rot) - ####plt.show() - ###img_rot[img_rot!=0]=1 - ####neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - ####print(var_spectrum,'var_spectrum') - ###try: - ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - #####print(rot,var_spectrum,'var_spectrum') - ###except: - ###var_spectrum=0 - ###var_res.append(var_spectrum) - - - - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 - - - angels=np.linspace(ang_int-22.5,ang_int+22.5,n_tot_angles) - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - ##var_res=[] - ##for rot in angels: - ##img_rot=rotate_image(img_resized,rot) - ####plt.imshow(img_rot) - ####plt.show() - ##img_rot[img_rot!=0]=1 - ##try: - ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ##except: - ##var_spectrum=0 - ##var_res.append(var_spectrum) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 - - elif main_page and img_patch_org.shape[1]<=img_patch_org.shape[0]: - + elif main_page: #plt.imshow(img_resized) #plt.show() - angels=np.linspace(-12,12,n_tot_angles)#np.array([0 , 45 , 90 , -45]) - - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - - ##var_res=[] - - ##for rot in angels: - ##img_rot=rotate_image(img_resized,rot) - ###plt.imshow(img_rot) - ###plt.show() - ##img_rot[img_rot!=0]=1 - ###neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - ###print(var_spectrum,'var_spectrum') - ##try: - ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - - ##except: - ##var_spectrum=0 - - ##var_res.append(var_spectrum) - - - if plotter: - plotter.save_plot_of_rotation_angle(angels, var_res) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 + angles = np.linspace(-12, 12, n_tot_angles)#np.array([0 , 45 , 90 , -45]) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) early_slope_edge=11 - if abs(ang_int)>early_slope_edge and ang_int<0: - angels=np.linspace(-90,-12,n_tot_angles) - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - ##var_res=[] - ##for rot in angels: - ##img_rot=rotate_image(img_resized,rot) - ####plt.imshow(img_rot) - ####plt.show() - ##img_rot[img_rot!=0]=1 - ##try: - ##var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ##except: - ##var_spectrum=0 - ##var_res.append(var_spectrum) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 + if abs(angle) > early_slope_edge: + if angle < 0: + angles = np.linspace(-90, -12, n_tot_angles) + else: + angles = np.linspace(90, 12, n_tot_angles) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) - elif abs(ang_int)>early_slope_edge and ang_int>0: - - angels=np.linspace(90,12,n_tot_angles) - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - - ###var_res=[] - ###for rot in angels: - ###img_rot=rotate_image(img_resized,rot) - #####plt.imshow(img_rot) - #####plt.show() - ###img_rot[img_rot!=0]=1 - ###try: - ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ####print(indexer,'indexer') - ###except: - ###var_spectrum=0 - ###var_res.append(var_spectrum) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 else: - angels=np.linspace(-25,25,int(n_tot_angles/2.)+10) - indexer=0 - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - ####var_res=[] - - ####for rot in angels: - ####img_rot=rotate_image(img_resized,rot) - #####plt.imshow(img_rot) - #####plt.show() - ####img_rot[img_rot!=0]=1 - #####neg_peaks,var_spectrum=self.find_num_col_deskew(img_rot,sigma_des,20.3 ) - #####print(var_spectrum,'var_spectrum') - ####try: - ####var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ####except: - ####var_spectrum=0 - ####var_res.append(var_spectrum) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 - - #plt.plot(var_res) - #plt.show() - ##plt.plot(mom3_res) - ##plt.show() - #print(ang_int,'ang_int111') + angles = np.linspace(-25, 25, int(0.5 * n_tot_angles) + 10) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) early_slope_edge=22 - if abs(ang_int)>early_slope_edge and ang_int<0: + if abs(angle) > early_slope_edge: + if angle < 0: + angles = np.linspace(-90, -25, int(0.5 * n_tot_angles) + 10) + else: + angles = np.linspace(90, 25, int(0.5 * n_tot_angles) + 10) + angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) - angels=np.linspace(-90,-25,int(n_tot_angles/2.)+10) - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - ###var_res=[] - - ###for rot in angels: - ###img_rot=rotate_image(img_resized,rot) - #####plt.imshow(img_rot) - #####plt.show() - ###img_rot[img_rot!=0]=1 - ###try: - ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ###except: - ###var_spectrum=0 - ###var_res.append(var_spectrum) - - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 - - elif abs(ang_int)>early_slope_edge and ang_int>0: - - angels=np.linspace(90,25,int(n_tot_angles/2.)+10) - indexer=0 - - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(angels), num_cores + 1) - - for i in range(num_cores): - angels_per_process = angels[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_image_rotation, args=(queue_of_all_params, angels_per_process, img_resized, sigma_des))) - - for i in range(num_cores): - processes[i].start() - - var_res=[] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - angles_for_subprocess = list_all_par[0] - for j in range(len(angles_for_subprocess)): - var_res.append(angles_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - - ###var_res=[] - - - ###for rot in angels: - ###img_rot=rotate_image(img_resized,rot) - #####plt.imshow(img_rot) - #####plt.show() - ###img_rot[img_rot!=0]=1 - ###try: - ###var_spectrum=find_num_col_deskew(img_rot,sigma_des,20.3 ) - ####print(indexer,'indexer') - ###except: - ###var_spectrum=0 - - ###var_res.append(var_spectrum) - try: - var_res=np.array(var_res) - ang_int=angels[np.argmax(var_res)]#angels_sorted[arg_final]#angels[arg_sort_early[arg_sort[arg_final]]]#angels[arg_fin] - except: - ang_int=0 - - return ang_int + return angle +def get_smallest_skew(img, sigma_des, angles, num_cores=1, plotter=None): + with Pool(processes=num_cores) as pool: + results = pool.map(partial(do_image_rotation, img=img, sigma_des=sigma_des), angles) + if plotter: + plotter.save_plot_of_rotation_angle(angles, results) + try: + var_res = np.array(results) + angle = angles[np.argmax(var_res)] + except: + angle = 0 + return angle From 5e0c1da7111690bd4898d928bfcde0c1de39c3b9 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 00:18:58 +0000 Subject: [PATCH 299/412] simplify --- src/eynollah/eynollah.py | 79 +++++++++++++--------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 90824c8..3b43f7b 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -823,8 +823,8 @@ class Eynollah: def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction") - img_height_model = model.layers[len(model.layers) - 1].output_shape[1] - img_width_model = model.layers[len(model.layers) - 1].output_shape[2] + img_height_model = model.layers[-1].output_shape[1] + img_width_model = model.layers[-1].output_shape[2] if not patches: img_h_page = img.shape[0] @@ -1034,8 +1034,8 @@ class Eynollah: def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction_new_concept") - img_height_model = model.layers[len(model.layers) - 1].output_shape[1] - img_width_model = model.layers[len(model.layers) - 1].output_shape[2] + img_height_model = model.layers[-1].output_shape[1] + img_width_model = model.layers[-1].output_shape[2] if not patches: img_h_page = img.shape[0] @@ -1043,7 +1043,7 @@ class Eynollah: img = img / 255.0 img = resize_image(img, img_height_model, img_width_model) - label_p_pred = model.predict(img.reshape(1, img.shape[0], img.shape[1], img.shape[2]), verbose=0) + label_p_pred = model.predict(img[np.newaxis], verbose=0) seg = np.argmax(label_p_pred, axis=3)[0] if thresholding_for_artificial_class_in_light_version: @@ -4928,31 +4928,31 @@ class Eynollah: #print("text region early 2 in %.1fs", time.time() - t0) ###min_con_area = 0.000005 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) @@ -5018,35 +5018,6 @@ class Eynollah: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - else: - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - #try: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - #except: - #contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - #areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - #self.logger.debug('areas_cnt_text_parent %s', areas_cnt_text_parent) - # self.logger.debug('areas_cnt_text_parent_d %s', areas_cnt_text_parent_d) - # self.logger.debug('len(contours_only_text_parent) %s', len(contours_only_text_parent_d)) - else: - pass #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: From 21efea87116aeb7c89bea31a0227f614f19c9c6b Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 18:36:57 +0000 Subject: [PATCH 300/412] no del on function argument --- src/eynollah/utils/contour.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/eynollah/utils/contour.py b/src/eynollah/utils/contour.py index 8a92ace..c5d56b8 100644 --- a/src/eynollah/utils/contour.py +++ b/src/eynollah/utils/contour.py @@ -39,7 +39,6 @@ def get_text_region_boxes_by_given_contours(contours): boxes.append([x, y, w, h]) contours_new.append(contours[jj]) - del contours return boxes, contours_new def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area): From 25e967397d753a0fdfd1c4c9181cfc93f94414b7 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 11:24:56 +0000 Subject: [PATCH 301/412] exit early if no text regions found (to avoid segfault) --- src/eynollah/eynollah.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 3b43f7b..d6ba8a9 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -5019,6 +5019,20 @@ class Eynollah: contours_only_text_parent_d = [] contours_only_text_parent = [] + if not len(contours_only_text_parent): + # stop early + empty_marginals = [[]] * len(polygons_of_marginals) + if self.full_layout: + pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) + else: + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) + self.logger.info("Job done in %.1fs", time.time() - t0) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) @@ -5164,10 +5178,12 @@ class Eynollah: all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: return pcgts - else: contours_only_text_parent_h = None if self.reading_order_machine_based: From 68456ea0022b47f50fb7e2614358879b6484d0b0 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 11:30:38 +0000 Subject: [PATCH 302/412] do_work_of_slopes_new*, do_back_rotation_and_get_cnt_back, do_work_of_contours_in_image: use mp.Pool, simplify --- src/eynollah/eynollah.py | 454 +++++---------------------- src/eynollah/utils/contour.py | 176 +++-------- src/eynollah/utils/separate_lines.py | 207 +++++++++++- 3 files changed, 324 insertions(+), 513 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index d6ba8a9..ae292c6 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -11,8 +11,9 @@ import os import sys import time import warnings +from functools import partial from pathlib import Path -from multiprocessing import Process, Queue, cpu_count +from multiprocessing import Pool, cpu_count import gc from ocrd_utils import getLogger import cv2 @@ -60,14 +61,20 @@ from .utils.contour import ( from .utils.rotate import ( rotate_image, rotation_not_90_func, - rotation_not_90_func_full_layout) + rotation_not_90_func_full_layout +) from .utils.separate_lines import ( textline_contours_postprocessing, separate_lines_new2, - return_deskew_slop) + return_deskew_slop, + do_work_of_slopes_new, + do_work_of_slopes_new_curved, + do_work_of_slopes_new_light, +) from .utils.drop_capitals import ( adhere_drop_capital_region_into_corresponding_textline, - filter_small_drop_capitals_from_no_patch_layout) + filter_small_drop_capitals_from_no_patch_layout +) from .utils.marginals import get_marginals from .utils.resize import resize_image from .utils import ( @@ -82,7 +89,8 @@ from .utils import ( small_textlines_to_parent_adherence2, order_of_regions, find_number_of_columns_in_document, - return_boxes_of_images_by_order_of_reading_new) + return_boxes_of_images_by_order_of_reading_new +) from .utils.pil_cv2 import check_dpi, pil2cv from .utils.xml import order_and_id_of_texts from .plot import EynollahPlotter @@ -1504,381 +1512,73 @@ class Eynollah: all_box_coord.append(crop_coor) - return slopes, all_found_textline_polygons, boxes, contours, contours_par, all_box_coord, np.array(range(len(contours_par))) + return all_found_textline_polygons, boxes, contours, contours_par, all_box_coord, np.array(range(len(contours_par))), slopes def get_slopes_and_deskew_new_light(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): - self.logger.debug("enter get_slopes_and_deskew_new") + if not len(contours): + return [], [], [], [], [], [], [] + self.logger.debug("enter get_slopes_and_deskew_new_light") if len(contours)>15: num_cores = cpu_count() else: num_cores = 1 - queue_of_all_params = Queue() - - processes = [] - nh = np.linspace(0, len(boxes), num_cores + 1) - indexes_by_text_con = np.array(range(len(contours_par))) - for i in range(num_cores): - boxes_per_process = boxes[int(nh[i]) : int(nh[i + 1])] - contours_per_process = contours[int(nh[i]) : int(nh[i + 1])] - contours_par_per_process = contours_par[int(nh[i]) : int(nh[i + 1])] - indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] - - processes.append(Process(target=self.do_work_of_slopes_new_light, args=(queue_of_all_params, boxes_per_process, textline_mask_tot, contours_per_process, contours_par_per_process, indexes_text_con_per_process, image_page_rotated, slope_deskew))) - for i in range(num_cores): - processes[i].start() - - slopes = [] - all_found_textline_polygons = [] - all_found_text_regions = [] - all_found_text_regions_par = [] - boxes = [] - all_box_coord = [] - all_index_text_con = [] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - slopes_for_sub_process = list_all_par[0] - polys_for_sub_process = list_all_par[1] - boxes_for_sub_process = list_all_par[2] - contours_for_subprocess = list_all_par[3] - contours_par_for_subprocess = list_all_par[4] - boxes_coord_for_subprocess = list_all_par[5] - indexes_for_subprocess = list_all_par[6] - for j in range(len(slopes_for_sub_process)): - slopes.append(slopes_for_sub_process[j]) - all_found_textline_polygons.append(polys_for_sub_process[j]) - boxes.append(boxes_for_sub_process[j]) - all_found_text_regions.append(contours_for_subprocess[j]) - all_found_text_regions_par.append(contours_par_for_subprocess[j]) - all_box_coord.append(boxes_coord_for_subprocess[j]) - all_index_text_con.append(indexes_for_subprocess[j]) - for i in range(num_cores): - processes[i].join() - self.logger.debug('slopes %s', slopes) - self.logger.debug("exit get_slopes_and_deskew_new") - return slopes, all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con + with Pool(processes=num_cores) as pool: + results = pool.starmap( + partial(do_work_of_slopes_new_light, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + slope_deskew=slope_deskew, + logger=self.logger, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + plotter=self.plotter,), + zip(boxes, contours, contours_par, range(len(contours_par)))) + #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) + self.logger.debug("exit get_slopes_and_deskew_new_light") + return tuple(zip(*results)) def get_slopes_and_deskew_new(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): + if not len(contours): + return [], [], [], [], [], [], [] self.logger.debug("enter get_slopes_and_deskew_new") num_cores = cpu_count() - queue_of_all_params = Queue() - - processes = [] - nh = np.linspace(0, len(boxes), num_cores + 1) - indexes_by_text_con = np.array(range(len(contours_par))) - for i in range(num_cores): - boxes_per_process = boxes[int(nh[i]) : int(nh[i + 1])] - contours_per_process = contours[int(nh[i]) : int(nh[i + 1])] - contours_par_per_process = contours_par[int(nh[i]) : int(nh[i + 1])] - indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] - - processes.append(Process(target=self.do_work_of_slopes_new, args=(queue_of_all_params, boxes_per_process, textline_mask_tot, contours_per_process, contours_par_per_process, indexes_text_con_per_process, image_page_rotated, slope_deskew))) - for i in range(num_cores): - processes[i].start() - - slopes = [] - all_found_textline_polygons = [] - all_found_text_regions = [] - all_found_text_regions_par = [] - boxes = [] - all_box_coord = [] - all_index_text_con = [] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - slopes_for_sub_process = list_all_par[0] - polys_for_sub_process = list_all_par[1] - boxes_for_sub_process = list_all_par[2] - contours_for_subprocess = list_all_par[3] - contours_par_for_subprocess = list_all_par[4] - boxes_coord_for_subprocess = list_all_par[5] - indexes_for_subprocess = list_all_par[6] - for j in range(len(slopes_for_sub_process)): - slopes.append(slopes_for_sub_process[j]) - all_found_textline_polygons.append(polys_for_sub_process[j]) - boxes.append(boxes_for_sub_process[j]) - all_found_text_regions.append(contours_for_subprocess[j]) - all_found_text_regions_par.append(contours_par_for_subprocess[j]) - all_box_coord.append(boxes_coord_for_subprocess[j]) - all_index_text_con.append(indexes_for_subprocess[j]) - for i in range(num_cores): - processes[i].join() - self.logger.debug('slopes %s', slopes) + with Pool(processes=num_cores) as pool: + results = pool.starmap( + partial(do_work_of_slopes_new, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + slope_deskew=slope_deskew, + logger=self.logger, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + plotter=self.plotter,), + zip(boxes, contours, contours_par, range(len(contours_par)))) + #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) self.logger.debug("exit get_slopes_and_deskew_new") - return slopes, all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con + return tuple(zip(*results)) def get_slopes_and_deskew_new_curved(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, mask_texts_only, num_col, scale_par, slope_deskew): + if not len(contours): + return [], [], [], [], [], [], [] self.logger.debug("enter get_slopes_and_deskew_new_curved") num_cores = cpu_count() - queue_of_all_params = Queue() - - processes = [] - nh = np.linspace(0, len(boxes), num_cores + 1) - indexes_by_text_con = np.array(range(len(contours_par))) - - for i in range(num_cores): - boxes_per_process = boxes[int(nh[i]) : int(nh[i + 1])] - contours_per_process = contours[int(nh[i]) : int(nh[i + 1])] - contours_par_per_process = contours_par[int(nh[i]) : int(nh[i + 1])] - indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] - - processes.append(Process(target=self.do_work_of_slopes_new_curved, args=(queue_of_all_params, boxes_per_process, textline_mask_tot, contours_per_process, contours_par_per_process, image_page_rotated, mask_texts_only, num_col, scale_par, indexes_text_con_per_process, slope_deskew))) - - for i in range(num_cores): - processes[i].start() - - slopes = [] - all_found_textline_polygons = [] - all_found_text_regions = [] - all_found_text_regions_par = [] - boxes = [] - all_box_coord = [] - all_index_text_con = [] - - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - polys_for_sub_process = list_all_par[0] - boxes_for_sub_process = list_all_par[1] - contours_for_subprocess = list_all_par[2] - contours_par_for_subprocess = list_all_par[3] - boxes_coord_for_subprocess = list_all_par[4] - indexes_for_subprocess = list_all_par[5] - slopes_for_sub_process = list_all_par[6] - for j in range(len(polys_for_sub_process)): - slopes.append(slopes_for_sub_process[j]) - all_found_textline_polygons.append(polys_for_sub_process[j][::-1]) - boxes.append(boxes_for_sub_process[j]) - all_found_text_regions.append(contours_for_subprocess[j]) - all_found_text_regions_par.append(contours_par_for_subprocess[j]) - all_box_coord.append(boxes_coord_for_subprocess[j]) - all_index_text_con.append(indexes_for_subprocess[j]) - - for i in range(num_cores): - processes[i].join() - # print(slopes,'slopes') - return all_found_textline_polygons, boxes, all_found_text_regions, all_found_text_regions_par, all_box_coord, all_index_text_con, slopes - - def do_work_of_slopes_new_curved(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, image_page_rotated, mask_texts_only, num_col, scale_par, indexes_r_con_per_pro, slope_deskew): - self.logger.debug("enter do_work_of_slopes_new_curved") - slopes_per_each_subprocess = [] - bounding_box_of_textregion_per_each_subprocess = [] - textlines_rectangles_per_each_subprocess = [] - contours_textregion_per_each_subprocess = [] - contours_textregion_par_per_each_subprocess = [] - all_box_coord_per_process = [] - index_by_text_region_contours = [] - - textline_cnt_separated = np.zeros(textline_mask_tot_ea.shape) - - for mv in range(len(boxes_text)): - - all_text_region_raw = textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] - all_text_region_raw = all_text_region_raw.astype(np.uint8) - img_int_p = all_text_region_raw[:, :] - - # img_int_p=cv2.erode(img_int_p,KERNEL,iterations = 2) - # plt.imshow(img_int_p) - # plt.show() - - if img_int_p.shape[0] / img_int_p.shape[1] < 0.1: - slopes_per_each_subprocess.append(0) - slope_for_all = [slope_deskew][0] - else: - try: - textline_con, hierarchy = return_contours_of_image(img_int_p) - textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.0008) - y_diff_mean = find_contours_mean_y_diff(textline_con_fil) - if self.isNaN(y_diff_mean): - slope_for_all = MAX_SLOPE - else: - sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) - img_int_p[img_int_p > 0] = 1 - slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=self.plotter) - - if abs(slope_for_all) < 0.5: - slope_for_all = [slope_deskew][0] - - except Exception as why: - self.logger.error(why) - slope_for_all = MAX_SLOPE - - if slope_for_all == MAX_SLOPE: - slope_for_all = [slope_deskew][0] - slopes_per_each_subprocess.append(slope_for_all) - - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - _, crop_coor = crop_image_inside_box(boxes_text[mv], image_page_rotated) - - if abs(slope_for_all) < 45: - # all_box_coord.append(crop_coor) - textline_region_in_image = np.zeros(textline_mask_tot_ea.shape) - cnt_o_t_max = contours_par_per_process[mv] - x, y, w, h = cv2.boundingRect(cnt_o_t_max) - mask_biggest = np.zeros(mask_texts_only.shape) - mask_biggest = cv2.fillPoly(mask_biggest, pts=[cnt_o_t_max], color=(1, 1, 1)) - mask_region_in_patch_region = mask_biggest[y : y + h, x : x + w] - textline_biggest_region = mask_biggest * textline_mask_tot_ea - - # print(slope_for_all,'slope_for_all') - textline_rotated_separated = separate_lines_new2(textline_biggest_region[y : y + h, x : x + w], 0, num_col, slope_for_all, plotter=self.plotter) - - # new line added - ##print(np.shape(textline_rotated_separated),np.shape(mask_biggest)) - textline_rotated_separated[mask_region_in_patch_region[:, :] != 1] = 0 - # till here - - textline_cnt_separated[y : y + h, x : x + w] = textline_rotated_separated - textline_region_in_image[y : y + h, x : x + w] = textline_rotated_separated - - # plt.imshow(textline_region_in_image) - # plt.show() - # plt.imshow(textline_cnt_separated) - # plt.show() - - pixel_img = 1 - cnt_textlines_in_image = return_contours_of_interested_textline(textline_region_in_image, pixel_img) - - textlines_cnt_per_region = [] - for jjjj in range(len(cnt_textlines_in_image)): - mask_biggest2 = np.zeros(mask_texts_only.shape) - mask_biggest2 = cv2.fillPoly(mask_biggest2, pts=[cnt_textlines_in_image[jjjj]], color=(1, 1, 1)) - if num_col + 1 == 1: - mask_biggest2 = cv2.dilate(mask_biggest2, KERNEL, iterations=5) - else: - mask_biggest2 = cv2.dilate(mask_biggest2, KERNEL, iterations=4) - - pixel_img = 1 - mask_biggest2 = resize_image(mask_biggest2, int(mask_biggest2.shape[0] * scale_par), int(mask_biggest2.shape[1] * scale_par)) - cnt_textlines_in_image_ind = return_contours_of_interested_textline(mask_biggest2, pixel_img) - try: - textlines_cnt_per_region.append(cnt_textlines_in_image_ind[0]) - except Exception as why: - self.logger.error(why) - else: - add_boxes_coor_into_textlines = True - textlines_cnt_per_region = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contours_par_per_process[mv], boxes_text[mv], add_boxes_coor_into_textlines) - add_boxes_coor_into_textlines = False - # print(np.shape(textlines_cnt_per_region),'textlines_cnt_per_region') - - textlines_rectangles_per_each_subprocess.append(textlines_cnt_per_region) - bounding_box_of_textregion_per_each_subprocess.append(boxes_text[mv]) - contours_textregion_per_each_subprocess.append(contours_per_process[mv]) - contours_textregion_par_per_each_subprocess.append(contours_par_per_process[mv]) - all_box_coord_per_process.append(crop_coor) - - queue_of_all_params.put([textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours, slopes_per_each_subprocess]) - def do_work_of_slopes_new_light(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, indexes_r_con_per_pro, image_page_rotated, slope_deskew): - self.logger.debug('enter do_work_of_slopes_new_light') - slopes_per_each_subprocess = [] - bounding_box_of_textregion_per_each_subprocess = [] - textlines_rectangles_per_each_subprocess = [] - contours_textregion_per_each_subprocess = [] - contours_textregion_par_per_each_subprocess = [] - all_box_coord_per_process = [] - index_by_text_region_contours = [] - for mv in range(len(boxes_text)): - _, crop_coor = crop_image_inside_box(boxes_text[mv],image_page_rotated) - mask_textline = np.zeros((textline_mask_tot_ea.shape)) - mask_textline = cv2.fillPoly(mask_textline,pts=[contours_per_process[mv]],color=(1,1,1)) - all_text_region_raw = (textline_mask_tot_ea*mask_textline[:,:])[boxes_text[mv][1]:boxes_text[mv][1]+boxes_text[mv][3] , boxes_text[mv][0]:boxes_text[mv][0]+boxes_text[mv][2] ] - all_text_region_raw=all_text_region_raw.astype(np.uint8) - - slopes_per_each_subprocess.append([slope_deskew][0]) - mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) - mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) - - - if self.textline_light: - all_text_region_raw = np.copy(textline_mask_tot_ea) - all_text_region_raw[mask_only_con_region == 0] = 0 - cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(all_text_region_raw) - cnt_clean_rot = filter_contours_area_of_image(all_text_region_raw, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) - else: - all_text_region_raw = np.copy(textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]]) - mask_only_con_region = mask_only_con_region[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] - all_text_region_raw[mask_only_con_region == 0] = 0 - cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, [slope_deskew][0], contours_par_per_process[mv], boxes_text[mv]) - - textlines_rectangles_per_each_subprocess.append(cnt_clean_rot) - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - bounding_box_of_textregion_per_each_subprocess.append(boxes_text[mv]) - - contours_textregion_per_each_subprocess.append(contours_per_process[mv]) - contours_textregion_par_per_each_subprocess.append(contours_par_per_process[mv]) - all_box_coord_per_process.append(crop_coor) - queue_of_all_params.put([slopes_per_each_subprocess, textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours]) - - def do_work_of_slopes_new(self, queue_of_all_params, boxes_text, textline_mask_tot_ea, contours_per_process, contours_par_per_process, indexes_r_con_per_pro, image_page_rotated, slope_deskew): - self.logger.debug('enter do_work_of_slopes_new') - slopes_per_each_subprocess = [] - bounding_box_of_textregion_per_each_subprocess = [] - textlines_rectangles_per_each_subprocess = [] - contours_textregion_per_each_subprocess = [] - contours_textregion_par_per_each_subprocess = [] - all_box_coord_per_process = [] - index_by_text_region_contours = [] - for mv in range(len(boxes_text)): - _, crop_coor = crop_image_inside_box(boxes_text[mv],image_page_rotated) - mask_textline = np.zeros((textline_mask_tot_ea.shape)) - mask_textline = cv2.fillPoly(mask_textline,pts=[contours_per_process[mv]],color=(1,1,1)) - all_text_region_raw = (textline_mask_tot_ea*mask_textline[:,:])[boxes_text[mv][1]:boxes_text[mv][1]+boxes_text[mv][3] , boxes_text[mv][0]:boxes_text[mv][0]+boxes_text[mv][2] ] - all_text_region_raw=all_text_region_raw.astype(np.uint8) - img_int_p=all_text_region_raw[:,:]#self.all_text_region_raw[mv] - img_int_p=cv2.erode(img_int_p,KERNEL,iterations = 2) - - if img_int_p.shape[0]/img_int_p.shape[1]<0.1: - slopes_per_each_subprocess.append(0) - slope_for_all = [slope_deskew][0] - all_text_region_raw = textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] - cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contours_par_per_process[mv], boxes_text[mv], 0) - textlines_rectangles_per_each_subprocess.append(cnt_clean_rot) - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - bounding_box_of_textregion_per_each_subprocess.append(boxes_text[mv]) - else: - try: - textline_con, hierarchy = return_contours_of_image(img_int_p) - textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.00008) - y_diff_mean = find_contours_mean_y_diff(textline_con_fil) - if self.isNaN(y_diff_mean): - slope_for_all = MAX_SLOPE - else: - sigma_des = int(y_diff_mean * (4.0 / 40.0)) - if sigma_des < 1: - sigma_des = 1 - img_int_p[img_int_p > 0] = 1 - slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=self.plotter) - if abs(slope_for_all) <= 0.5: - slope_for_all = [slope_deskew][0] - except Exception as why: - self.logger.error(why) - slope_for_all = MAX_SLOPE - if slope_for_all == MAX_SLOPE: - slope_for_all = [slope_deskew][0] - slopes_per_each_subprocess.append(slope_for_all) - mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) - mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) - - # plt.imshow(mask_only_con_region) - # plt.show() - all_text_region_raw = np.copy(textline_mask_tot_ea[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]]) - mask_only_con_region = mask_only_con_region[boxes_text[mv][1] : boxes_text[mv][1] + boxes_text[mv][3], boxes_text[mv][0] : boxes_text[mv][0] + boxes_text[mv][2]] - - ##plt.imshow(textline_mask_tot_ea) - ##plt.show() - ##plt.imshow(all_text_region_raw) - ##plt.show() - ##plt.imshow(mask_only_con_region) - ##plt.show() - - all_text_region_raw[mask_only_con_region == 0] = 0 - cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contours_par_per_process[mv], boxes_text[mv]) - - textlines_rectangles_per_each_subprocess.append(cnt_clean_rot) - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - bounding_box_of_textregion_per_each_subprocess.append(boxes_text[mv]) - - contours_textregion_per_each_subprocess.append(contours_per_process[mv]) - contours_textregion_par_per_each_subprocess.append(contours_par_per_process[mv]) - all_box_coord_per_process.append(crop_coor) - queue_of_all_params.put([slopes_per_each_subprocess, textlines_rectangles_per_each_subprocess, bounding_box_of_textregion_per_each_subprocess, contours_textregion_per_each_subprocess, contours_textregion_par_per_each_subprocess, all_box_coord_per_process, index_by_text_region_contours]) + with Pool(processes=num_cores) as pool: + results = pool.starmap( + partial(do_work_of_slopes_new_curved, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + mask_texts_only=mask_texts_only, + num_col=num_col, + scale_par=scale_par, + slope_deskew=slope_deskew, + logger=self.logger, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + plotter=self.plotter,), + zip(boxes, contours, contours_par, range(len(contours_par)))) + #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) + self.logger.debug("exit get_slopes_and_deskew_new_curved") + return tuple(zip(*results)) def textline_contours(self, img, use_patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') @@ -1923,6 +1623,7 @@ class Eynollah: prediction_textline_longshot = self.do_prediction(False, img, self.model_textline) prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) + self.logger.debug('exit textline_contours') return ((prediction_textline[:, :, 0]==1)*1).astype('uint8'), ((prediction_textline_longshot_true_size[:, :, 0]==1)*1).astype('uint8') @@ -1959,6 +1660,7 @@ class Eynollah: q.put(slopes_sub) poly.put(poly_sub) box_sub.put(boxes_sub_new) + self.logger.debug('exit do_work_of_slopes') def get_regions_light_v_extract_only_images(self,img,is_image_enhanced, num_col_classifier): self.logger.debug("enter get_regions_extract_images_only") @@ -2069,6 +1771,7 @@ class Eynollah: polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) + self.logger.debug("exit get_regions_extract_images_only") return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin, image_page, page_coord, cont_page def get_regions_light_v(self,img,is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=False): @@ -2146,6 +1849,7 @@ class Eynollah: #print("inside 1 ", time.time()-t_in) ###textline_mask_tot_ea = self.run_textline(img_bin) + self.logger.debug("detecting textlines on %s with %d colors", str(img_resized.shape), len(np.unique(img_resized))) textline_mask_tot_ea = self.run_textline(img_resized, num_col_classifier) @@ -2269,9 +1973,11 @@ class Eynollah: #plt.imshow(textline_mask_tot_ea) #plt.show() #print("inside 4 ", time.time()-t_in) + self.logger.debug("exit get_regions_light_v") return text_regions_p_true, erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin else: img_bin = resize_image(img_bin,img_height_h, img_width_h ) + self.logger.debug("exit get_regions_light_v") return None, erosion_hurts, None, textline_mask_tot_ea, img_bin def get_regions_from_xy_2models(self,img,is_image_enhanced, num_col_classifier): @@ -2392,6 +2098,7 @@ class Eynollah: text_regions_p_true=cv2.fillPoly(text_regions_p_true,pts=polygons_of_only_texts, color=(1,1,1)) + self.logger.debug("exit get_regions_from_xy_2models") return text_regions_p_true, erosion_hurts, polygons_lines_xml except: @@ -2461,6 +2168,7 @@ class Eynollah: text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) erosion_hurts = True + self.logger.debug("exit get_regions_from_xy_2models") return text_regions_p_true, erosion_hurts, polygons_lines_xml def do_order_of_regions_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -2633,6 +2341,7 @@ class Eynollah: for iii in range(len(order_of_texts_tot)): order_text_new.append(np.where(np.array(order_of_texts_tot) == iii)[0][0]) + self.logger.debug("exit do_order_of_regions_full_layout") return order_text_new, id_of_texts_tot def do_order_of_regions_no_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): @@ -2743,6 +2452,7 @@ class Eynollah: for iii in range(len(order_of_texts_tot)): order_text_new.append(np.where(np.array(order_of_texts_tot) == iii)[0][0]) + self.logger.debug("exit do_order_of_regions_no_full_layout") return order_text_new, id_of_texts_tot def check_iou_of_bounding_box_and_contour_for_tables(self, layout, table_prediction_early, pixel_tabel, num_col_classifier): layout_org = np.copy(layout) @@ -5051,12 +4761,12 @@ class Eynollah: if not self.curved_line: if self.light_version: if self.textline_light: - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ @@ -5074,17 +4784,17 @@ class Eynollah: else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con = \ + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _ = \ + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: scale_param = 1 diff --git a/src/eynollah/utils/contour.py b/src/eynollah/utils/contour.py index c5d56b8..65331c2 100644 --- a/src/eynollah/utils/contour.py +++ b/src/eynollah/utils/contour.py @@ -1,10 +1,11 @@ +from functools import partial +from multiprocessing import cpu_count, Pool import cv2 import numpy as np from shapely import geometry from .rotate import rotate_image, rotation_image_new -from multiprocessing import Process, Queue, cpu_count -from multiprocessing import Pool + def contours_in_same_horizon(cy_main_hor): X1 = np.zeros((len(cy_main_hor), len(cy_main_hor))) X2 = np.zeros((len(cy_main_hor), len(cy_main_hor))) @@ -29,7 +30,6 @@ def find_contours_mean_y_diff(contours_main): def get_text_region_boxes_by_given_contours(contours): - kernel = np.ones((5, 5), np.uint8) boxes = [] contours_new = [] @@ -144,73 +144,11 @@ def return_contours_of_interested_region(region_pre_p, pixel, min_area=0.0002): return contours_imgs -def do_work_of_contours_in_image(queue_of_all_params, contours_per_process, indexes_r_con_per_pro, img, slope_first): - cnts_org_per_each_subprocess = [] - index_by_text_region_contours = [] - for mv in range(len(contours_per_process)): - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - - img_copy = np.zeros(img.shape) - img_copy = cv2.fillPoly(img_copy, pts=[contours_per_process[mv]], color=(1, 1, 1)) - - img_copy = rotation_image_new(img_copy, -slope_first) - - img_copy = img_copy.astype(np.uint8) - imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) - - cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - - cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) - cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) - - - cnts_org_per_each_subprocess.append(cont_int[0]) - - queue_of_all_params.put([ cnts_org_per_each_subprocess, index_by_text_region_contours]) - - -def get_textregion_contours_in_org_image_multi(cnts, img, slope_first): - - num_cores = cpu_count() - queue_of_all_params = Queue() - - processes = [] - nh = np.linspace(0, len(cnts), num_cores + 1) - indexes_by_text_con = np.array(range(len(cnts))) - for i in range(num_cores): - contours_per_process = cnts[int(nh[i]) : int(nh[i + 1])] - indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] - - processes.append(Process(target=do_work_of_contours_in_image, args=(queue_of_all_params, contours_per_process, indexes_text_con_per_process, img,slope_first ))) - for i in range(num_cores): - processes[i].start() - cnts_org = [] - all_index_text_con = [] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - contours_for_sub_process = list_all_par[0] - indexes_for_sub_process = list_all_par[1] - for j in range(len(contours_for_sub_process)): - cnts_org.append(contours_for_sub_process[j]) - all_index_text_con.append(indexes_for_sub_process[j]) - for i in range(num_cores): - processes[i].join() - - print(all_index_text_con) - return cnts_org -def loop_contour_image(index_l, cnts,img, slope_first): +def do_work_of_contours_in_image(contour, index_r_con, img, slope_first): img_copy = np.zeros(img.shape) - img_copy = cv2.fillPoly(img_copy, pts=[cnts[index_l]], color=(1, 1, 1)) + img_copy = cv2.fillPoly(img_copy, pts=[contour], color=(1, 1, 1)) - # plt.imshow(img_copy) - # plt.show() - - # print(img.shape,'img') img_copy = rotation_image_new(img_copy, -slope_first) - ##print(img_copy.shape,'img_copy') - # plt.imshow(img_copy) - # plt.show() img_copy = img_copy.astype(np.uint8) imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) @@ -220,17 +158,22 @@ def loop_contour_image(index_l, cnts,img, slope_first): cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) - # print(np.shape(cont_int[0])) - return cont_int[0] -def get_textregion_contours_in_org_image_multi2(cnts, img, slope_first): - cnts_org = [] - # print(cnts,'cnts') - with Pool(cpu_count()) as p: - cnts_org = p.starmap(loop_contour_image, [(index_l,cnts, img,slope_first) for index_l in range(len(cnts))]) - - return cnts_org + return cont_int[0], index_r_con + +def get_textregion_contours_in_org_image_multi(cnts, img, slope_first): + if not len(cnts): + return [], [] + num_cores = cpu_count() + with Pool(processes=num_cores) as pool: + results = pool.starmap( + partial(do_work_of_contours_in_image, + img=img, + slope_first=slope_first, + ), + zip(cnts, range(len(cnts)))) + return tuple(zip(*results)) def get_textregion_contours_in_org_image(cnts, img, slope_first): @@ -292,69 +235,40 @@ def get_textregion_contours_in_org_image_light_old(cnts, img, slope_first): return cnts_org -def return_list_of_contours_with_desired_order(ls_cons, sorted_indexes): - return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] -def do_back_rotation_and_get_cnt_back(queue_of_all_params, contours_par_per_process,indexes_r_con_per_pro, img, slope_first): - contours_textregion_per_each_subprocess = [] - index_by_text_region_contours = [] - for mv in range(len(contours_par_per_process)): - img_copy = np.zeros(img.shape) - img_copy = cv2.fillPoly(img_copy, pts=[contours_par_per_process[mv]], color=(1, 1, 1)) +def do_back_rotation_and_get_cnt_back(contour_par, index_r_con, img, slope_first): + img_copy = np.zeros(img.shape) + img_copy = cv2.fillPoly(img_copy, pts=[contour_par], color=(1, 1, 1)) - img_copy = rotation_image_new(img_copy, -slope_first) + img_copy = rotation_image_new(img_copy, -slope_first) - img_copy = img_copy.astype(np.uint8) - imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(imgray, 0, 255, 0) + img_copy = img_copy.astype(np.uint8) + imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(imgray, 0, 255, 0) - cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) + cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) - cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) - # print(np.shape(cont_int[0])) - contours_textregion_per_each_subprocess.append(cont_int[0]*6) - index_by_text_region_contours.append(indexes_r_con_per_pro[mv]) - - queue_of_all_params.put([contours_textregion_per_each_subprocess, index_by_text_region_contours]) + cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) + cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) + # print(np.shape(cont_int[0])) + return cont_int[0], index_r_con def get_textregion_contours_in_org_image_light(cnts, img, slope_first): - num_cores = cpu_count() - queue_of_all_params = Queue() - processes = [] - nh = np.linspace(0, len(cnts), num_cores + 1) - indexes_by_text_con = np.array(range(len(cnts))) - - h_o = img.shape[0] - w_o = img.shape[1] - - img = cv2.resize(img, (int(img.shape[1]/6.), int(img.shape[0]/6.)), interpolation=cv2.INTER_NEAREST) + if not len(cnts): + return [] + img = cv2.resize(img, (int(img.shape[1]/6), int(img.shape[0]/6)), interpolation=cv2.INTER_NEAREST) ##cnts = list( (np.array(cnts)/2).astype(np.int16) ) #cnts = cnts/2 - cnts = [(i/ 6).astype(np.int32) for i in cnts] - - for i in range(num_cores): - contours_par_per_process = cnts[int(nh[i]) : int(nh[i + 1])] - indexes_text_con_per_process = indexes_by_text_con[int(nh[i]) : int(nh[i + 1])] - processes.append(Process(target=do_back_rotation_and_get_cnt_back, args=(queue_of_all_params, contours_par_per_process, indexes_text_con_per_process, img, slope_first))) - - for i in range(num_cores): - processes[i].start() - - cnts_org = [] - all_index_text_con = [] - for i in range(num_cores): - list_all_par = queue_of_all_params.get(True) - contours_for_subprocess = list_all_par[0] - indexes_for_subprocess = list_all_par[1] - for j in range(len(contours_for_subprocess)): - cnts_org.append(contours_for_subprocess[j]) - all_index_text_con.append(indexes_for_subprocess[j]) - for i in range(num_cores): - processes[i].join() - - cnts_org = return_list_of_contours_with_desired_order(cnts_org, all_index_text_con) - - return cnts_org + cnts = [(i/6).astype(np.int) for i in cnts] + num_cores = cpu_count() + with Pool(processes=num_cores) as pool: + results = pool.starmap( + partial(do_back_rotation_and_get_cnt_back, + img=img, + slope_first=slope_first, + ), + zip(cnts, range(len(cnts)))) + contours, indexes = tuple(zip(*results)) + return [i*6 for i in contours] def return_contours_of_interested_textline(region_pre_p, pixel): diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index 36a1b01..922fa14 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1,22 +1,23 @@ +import os from functools import partial +from multiprocessing import Pool, cpu_count import numpy as np import cv2 from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d -import os -from multiprocessing import Process, Queue, cpu_count -from multiprocessing import Pool from .rotate import rotate_image +from .resize import resize_image from .contour import ( return_parent_contours, filter_contours_area_of_image_tables, return_contours_of_image, - filter_contours_area_of_image + filter_contours_area_of_image, + return_contours_of_interested_textline, + find_contours_mean_y_diff, ) -from .is_nan import isNaN from . import ( find_num_col_deskew, - isNaN, + crop_image_inside_box, ) def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): @@ -1249,13 +1250,13 @@ def separate_lines_new_inside_tiles(img_path, thetha): forest.append(peaks_neg[i + 1]) if diff_peaks[i] > cut_off: # print(forest[np.argmin(z[forest]) ] ) - if not isNaN(forest[np.argmin(z[forest])]): + if not np.isnan(forest[np.argmin(z[forest])]): peaks_neg_true.append(forest[np.argmin(z[forest])]) forest = [] forest.append(peaks_neg[i + 1]) if i == (len(peaks_neg) - 1): # print(print(forest[np.argmin(z[forest]) ] )) - if not isNaN(forest[np.argmin(z[forest])]): + if not np.isnan(forest[np.argmin(z[forest])]): peaks_neg_true.append(forest[np.argmin(z[forest])]) diff_peaks_pos = np.abs(np.diff(peaks)) @@ -1272,13 +1273,13 @@ def separate_lines_new_inside_tiles(img_path, thetha): forest.append(peaks[i + 1]) if diff_peaks_pos[i] > cut_off: # print(forest[np.argmin(z[forest]) ] ) - if not isNaN(forest[np.argmax(z[forest])]): + if not np.isnan(forest[np.argmax(z[forest])]): peaks_pos_true.append(forest[np.argmax(z[forest])]) forest = [] forest.append(peaks[i + 1]) if i == (len(peaks) - 1): # print(print(forest[np.argmin(z[forest]) ] )) - if not isNaN(forest[np.argmax(z[forest])]): + if not np.isnan(forest[np.argmax(z[forest])]): peaks_pos_true.append(forest[np.argmax(z[forest])]) # print(len(peaks_neg_true) ,len(peaks_pos_true) ,'lensss') @@ -1658,3 +1659,189 @@ def get_smallest_skew(img, sigma_des, angles, num_cores=1, plotter=None): except: angle = 0 return angle + +def do_work_of_slopes_new( + box_text, contour, contour_par, index_r_con, + textline_mask_tot_ea, image_page_rotated, slope_deskew, + logger, MAX_SLOPE=999, KERNEL=None, plotter=None +): + logger.debug('enter do_work_of_slopes_new') + if KERNEL is None: + KERNEL = np.ones((5, 5), np.uint8) + + x, y, w, h = box_text + _, crop_coor = crop_image_inside_box(box_text, image_page_rotated) + mask_textline = np.zeros(textline_mask_tot_ea.shape) + mask_textline = cv2.fillPoly(mask_textline, pts=[contour], color=(1,1,1)) + all_text_region_raw = textline_mask_tot_ea * mask_textline + all_text_region_raw = all_text_region_raw[y: y + h, x: x + w].astype(np.uint8) + img_int_p = all_text_region_raw[:,:] + img_int_p = cv2.erode(img_int_p, KERNEL, iterations=2) + + if img_int_p.shape[0] /img_int_p.shape[1] < 0.1: + slope = 0 + slope_for_all = slope_deskew + all_text_region_raw = textline_mask_tot_ea[y: y + h, x: x + w] + cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contour_par, box_text, 0) + else: + try: + textline_con, hierarchy = return_contours_of_image(img_int_p) + textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.00008) + y_diff_mean = find_contours_mean_y_diff(textline_con_fil) + if np.isnan(y_diff_mean): + slope_for_all = MAX_SLOPE + else: + sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) + img_int_p[img_int_p > 0] = 1 + slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=plotter) + if abs(slope_for_all) <= 0.5: + slope_for_all = slope_deskew + except Exception as why: + logger.error(why) + slope_for_all = MAX_SLOPE + + if slope_for_all == MAX_SLOPE: + slope_for_all = slope_deskew + slope = slope_for_all + + mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) + mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contour_par], color=(1, 1, 1)) + + # plt.imshow(mask_only_con_region) + # plt.show() + all_text_region_raw = textline_mask_tot_ea[y: y + h, x: x + w].copy() + mask_only_con_region = mask_only_con_region[y: y + h, x: x + w] + + ##plt.imshow(textline_mask_tot_ea) + ##plt.show() + ##plt.imshow(all_text_region_raw) + ##plt.show() + ##plt.imshow(mask_only_con_region) + ##plt.show() + + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contour_par, box_text) + + return cnt_clean_rot, box_text, contour, contour_par, crop_coor, index_r_con, slope + + +def do_work_of_slopes_new_curved( + box_text, contour, contour_par, index_r_con, + textline_mask_tot_ea, image_page_rotated, mask_texts_only, num_col, scale_par, slope_deskew, + logger, MAX_SLOPE=999, KERNEL=None, plotter=None +): + logger.debug("enter do_work_of_slopes_new_curved") + if KERNEL is None: + KERNEL = np.ones((5, 5), np.uint8) + + x, y, w, h = box_text + all_text_region_raw = textline_mask_tot_ea[y: y + h, x: x + w].astype(np.uint8) + img_int_p = all_text_region_raw[:, :] + + # img_int_p=cv2.erode(img_int_p,KERNEL,iterations = 2) + # plt.imshow(img_int_p) + # plt.show() + + if img_int_p.shape[0] / img_int_p.shape[1] < 0.1: + slope = 0 + slope_for_all = slope_deskew + else: + try: + textline_con, hierarchy = return_contours_of_image(img_int_p) + textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.0008) + y_diff_mean = find_contours_mean_y_diff(textline_con_fil) + if np.isnan(y_diff_mean): + slope_for_all = MAX_SLOPE + else: + sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) + img_int_p[img_int_p > 0] = 1 + slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=plotter) + if abs(slope_for_all) < 0.5: + slope_for_all = slope_deskew + except Exception as why: + logger.error(why) + slope_for_all = MAX_SLOPE + + if slope_for_all == MAX_SLOPE: + slope_for_all = slope_deskew + slope = slope_for_all + + _, crop_coor = crop_image_inside_box(box_text, image_page_rotated) + + if abs(slope_for_all) < 45: + textline_region_in_image = np.zeros(textline_mask_tot_ea.shape) + x, y, w, h = cv2.boundingRect(contour_par) + mask_biggest = np.zeros(mask_texts_only.shape) + mask_biggest = cv2.fillPoly(mask_biggest, pts=[contour_par], color=(1, 1, 1)) + mask_region_in_patch_region = mask_biggest[y : y + h, x : x + w] + textline_biggest_region = mask_biggest * textline_mask_tot_ea + + # print(slope_for_all,'slope_for_all') + textline_rotated_separated = separate_lines_new2(textline_biggest_region[y: y+h, x: x+w], 0, num_col, slope_for_all, + plotter=plotter) + + # new line added + ##print(np.shape(textline_rotated_separated),np.shape(mask_biggest)) + textline_rotated_separated[mask_region_in_patch_region[:, :] != 1] = 0 + # till here + + textline_region_in_image[y : y + h, x : x + w] = textline_rotated_separated + + # plt.imshow(textline_region_in_image) + # plt.show() + + pixel_img = 1 + cnt_textlines_in_image = return_contours_of_interested_textline(textline_region_in_image, pixel_img) + + textlines_cnt_per_region = [] + for jjjj in range(len(cnt_textlines_in_image)): + mask_biggest2 = np.zeros(mask_texts_only.shape) + mask_biggest2 = cv2.fillPoly(mask_biggest2, pts=[cnt_textlines_in_image[jjjj]], color=(1, 1, 1)) + if num_col + 1 == 1: + mask_biggest2 = cv2.dilate(mask_biggest2, KERNEL, iterations=5) + else: + mask_biggest2 = cv2.dilate(mask_biggest2, KERNEL, iterations=4) + + pixel_img = 1 + mask_biggest2 = resize_image(mask_biggest2, int(mask_biggest2.shape[0] * scale_par), int(mask_biggest2.shape[1] * scale_par)) + cnt_textlines_in_image_ind = return_contours_of_interested_textline(mask_biggest2, pixel_img) + try: + textlines_cnt_per_region.append(cnt_textlines_in_image_ind[0]) + except Exception as why: + logger.error(why) + else: + textlines_cnt_per_region = textline_contours_postprocessing(all_text_region_raw, slope_for_all, contour_par, box_text, True) + # print(np.shape(textlines_cnt_per_region),'textlines_cnt_per_region') + + return textlines_cnt_per_region[::-1], box_text, contour, contour_par, crop_coor, index_r_con, slope + +def do_work_of_slopes_new_light( + box_text, contour, contour_par, index_r_con, + textline_mask_tot_ea, image_page_rotated, slope_deskew, + logger +): + logger.debug('enter do_work_of_slopes_new_light') + + x, y, w, h = box_text + _, crop_coor = crop_image_inside_box(box_text, image_page_rotated) + mask_textline = np.zeros(textline_mask_tot_ea.shape) + mask_textline = cv2.fillPoly(mask_textline, pts=[contour], color=(1,1,1)) + all_text_region_raw = textline_mask_tot_ea * mask_textline + all_text_region_raw = all_text_region_raw[y: y + h, x: x + w].astype(np.uint8) + + mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) + mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contour_par], color=(1, 1, 1)) + + if self.textline_light: + all_text_region_raw = np.copy(textline_mask_tot_ea) + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(all_text_region_raw) + cnt_clean_rot = filter_contours_area_of_image(all_text_region_raw, cnt_clean_rot_raw, hir_on_cnt_clean_rot, + max_area=1, min_area=0.00001) + else: + all_text_region_raw = np.copy(textline_mask_tot_ea[y: y + h, x: x + w]) + mask_only_con_region = mask_only_con_region[y: y + h, x: x + w] + all_text_region_raw[mask_only_con_region == 0] = 0 + cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_deskew, contour_par, box_text) + + return cnt_clean_rot, box_text, contour, contour_par, crop_coor, index_r_con, slope From 7e9ee90e6ec5e3e8455e75f24ec4a0c4e4b95d70 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 12:18:29 +0000 Subject: [PATCH 303/412] switch from (ad-hoc) mp.Pool to (attribute) concurrent.futures.ProcessPoolExecutor --- src/eynollah/eynollah.py | 88 ++++++++++++---------------- src/eynollah/utils/contour.py | 31 ++++------ src/eynollah/utils/separate_lines.py | 67 ++++++++++++--------- 3 files changed, 91 insertions(+), 95 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index ae292c6..8c92b92 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -13,7 +13,8 @@ import time import warnings from functools import partial from pathlib import Path -from multiprocessing import Pool, cpu_count +from multiprocessing import cpu_count +from concurrent.futures import ProcessPoolExecutor import gc from ocrd_utils import getLogger import cv2 @@ -251,6 +252,8 @@ class Eynollah: textline_light = self.textline_light, pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') + # for parallelization of CPU-intensive tasks: + self.executor = ProcessPoolExecutor(max_workers=cpu_count()) self.dir_models = dir_models self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425" self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425" @@ -1518,21 +1521,15 @@ class Eynollah: if not len(contours): return [], [], [], [], [], [], [] self.logger.debug("enter get_slopes_and_deskew_new_light") - if len(contours)>15: - num_cores = cpu_count() - else: - num_cores = 1 - with Pool(processes=num_cores) as pool: - results = pool.starmap( - partial(do_work_of_slopes_new_light, - textline_mask_tot_ea=textline_mask_tot, - image_page_rotated=image_page_rotated, - slope_deskew=slope_deskew, - logger=self.logger, - MAX_SLOPE=MAX_SLOPE, - KERNEL=KERNEL, - plotter=self.plotter,), - zip(boxes, contours, contours_par, range(len(contours_par)))) + results = self.executor.map(partial(do_work_of_slopes_new_light, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + slope_deskew=slope_deskew, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + logger=self.logger, + plotter=self.plotter,), + boxes, contours, contours_par, range(len(contours_par))) #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) self.logger.debug("exit get_slopes_and_deskew_new_light") return tuple(zip(*results)) @@ -1541,18 +1538,15 @@ class Eynollah: if not len(contours): return [], [], [], [], [], [], [] self.logger.debug("enter get_slopes_and_deskew_new") - num_cores = cpu_count() - with Pool(processes=num_cores) as pool: - results = pool.starmap( - partial(do_work_of_slopes_new, - textline_mask_tot_ea=textline_mask_tot, - image_page_rotated=image_page_rotated, - slope_deskew=slope_deskew, - logger=self.logger, - MAX_SLOPE=MAX_SLOPE, - KERNEL=KERNEL, - plotter=self.plotter,), - zip(boxes, contours, contours_par, range(len(contours_par)))) + results = self.executor.map(partial(do_work_of_slopes_new, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + slope_deskew=slope_deskew, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + logger=self.logger, + plotter=self.plotter,), + boxes, contours, contours_par, range(len(contours_par))) #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) self.logger.debug("exit get_slopes_and_deskew_new") return tuple(zip(*results)) @@ -1561,21 +1555,18 @@ class Eynollah: if not len(contours): return [], [], [], [], [], [], [] self.logger.debug("enter get_slopes_and_deskew_new_curved") - num_cores = cpu_count() - with Pool(processes=num_cores) as pool: - results = pool.starmap( - partial(do_work_of_slopes_new_curved, - textline_mask_tot_ea=textline_mask_tot, - image_page_rotated=image_page_rotated, - mask_texts_only=mask_texts_only, - num_col=num_col, - scale_par=scale_par, - slope_deskew=slope_deskew, - logger=self.logger, - MAX_SLOPE=MAX_SLOPE, - KERNEL=KERNEL, - plotter=self.plotter,), - zip(boxes, contours, contours_par, range(len(contours_par)))) + results = self.executor.map(partial(do_work_of_slopes_new_curved, + textline_mask_tot_ea=textline_mask_tot, + image_page_rotated=image_page_rotated, + mask_texts_only=mask_texts_only, + num_col=num_col, + scale_par=scale_par, + slope_deskew=slope_deskew, + MAX_SLOPE=MAX_SLOPE, + KERNEL=KERNEL, + logger=self.logger, + plotter=self.plotter,), + boxes, contours, contours_par, range(len(contours_par))) #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) self.logger.debug("exit get_slopes_and_deskew_new_curved") return tuple(zip(*results)) @@ -1643,7 +1634,8 @@ class Eynollah: y_diff_mean = find_contours_mean_y_diff(textline_con_fil) sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) crop_img[crop_img > 0] = 1 - slope_corresponding_textregion = return_deskew_slop(crop_img, sigma_des, plotter=self.plotter) + slope_corresponding_textregion = return_deskew_slop(crop_img, sigma_des, + map=self.executor.map, logger=self.logger, plotter=self.plotter) except Exception as why: self.logger.error(why) slope_corresponding_textregion = MAX_SLOPE @@ -2932,10 +2924,8 @@ class Eynollah: def run_deskew(self, textline_mask_tot_ea): #print(textline_mask_tot_ea.shape, 'textline_mask_tot_ea deskew') - sigma = 2 - main_page_deskew = True - n_total_angles = 30 - slope_deskew = return_deskew_slop(cv2.erode(textline_mask_tot_ea, KERNEL, iterations=2), sigma, n_total_angles, main_page_deskew, plotter=self.plotter) + slope_deskew = return_deskew_slop(cv2.erode(textline_mask_tot_ea, KERNEL, iterations=2), 2, 30, True, + map=self.executor.map, logger=self.logger, plotter=self.plotter) slope_first = 0 if self.plotter: @@ -4748,7 +4738,7 @@ class Eynollah: contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) #txt_con_org = self.dilate_textregions_contours(txt_con_org) #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: diff --git a/src/eynollah/utils/contour.py b/src/eynollah/utils/contour.py index 65331c2..e47c5e7 100644 --- a/src/eynollah/utils/contour.py +++ b/src/eynollah/utils/contour.py @@ -1,5 +1,4 @@ from functools import partial -from multiprocessing import cpu_count, Pool import cv2 import numpy as np from shapely import geometry @@ -162,17 +161,14 @@ def do_work_of_contours_in_image(contour, index_r_con, img, slope_first): return cont_int[0], index_r_con -def get_textregion_contours_in_org_image_multi(cnts, img, slope_first): +def get_textregion_contours_in_org_image_multi(cnts, img, slope_first, map=map): if not len(cnts): return [], [] - num_cores = cpu_count() - with Pool(processes=num_cores) as pool: - results = pool.starmap( - partial(do_work_of_contours_in_image, - img=img, - slope_first=slope_first, - ), - zip(cnts, range(len(cnts)))) + results = map(partial(do_work_of_contours_in_image, + img=img, + slope_first=slope_first, + ), + cnts, range(len(cnts))) return tuple(zip(*results)) def get_textregion_contours_in_org_image(cnts, img, slope_first): @@ -252,21 +248,18 @@ def do_back_rotation_and_get_cnt_back(contour_par, index_r_con, img, slope_first # print(np.shape(cont_int[0])) return cont_int[0], index_r_con -def get_textregion_contours_in_org_image_light(cnts, img, slope_first): +def get_textregion_contours_in_org_image_light(cnts, img, slope_first, map=map): if not len(cnts): return [] img = cv2.resize(img, (int(img.shape[1]/6), int(img.shape[0]/6)), interpolation=cv2.INTER_NEAREST) ##cnts = list( (np.array(cnts)/2).astype(np.int16) ) #cnts = cnts/2 cnts = [(i/6).astype(np.int) for i in cnts] - num_cores = cpu_count() - with Pool(processes=num_cores) as pool: - results = pool.starmap( - partial(do_back_rotation_and_get_cnt_back, - img=img, - slope_first=slope_first, - ), - zip(cnts, range(len(cnts)))) + results = map(partial(do_back_rotation_and_get_cnt_back, + img=img, + slope_first=slope_first, + ), + cnts, range(len(cnts))) contours, indexes = tuple(zip(*results)) return [i*6 for i in contours] diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index 922fa14..48e1c5b 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1,6 +1,6 @@ import os +from logging import getLogger from functools import partial -from multiprocessing import Pool, cpu_count import numpy as np import cv2 from scipy.signal import find_peaks @@ -1464,7 +1464,9 @@ def textline_contours_postprocessing(textline_mask, slope, contour_text_interest return contours_rotated_clean -def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): +def separate_lines_new2(img_path, thetha, num_col, slope_region, logger=None, plotter=None): + if logger is None: + logger = getLogger(__package__) if num_col == 1: num_patches = int(img_path.shape[1] / 200.0) @@ -1572,18 +1574,20 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, plotter=None): # plt.show() return img_patch_ineterst_revised -def do_image_rotation(angle, img, sigma_des): - print(f"rotating image by {angle}") +def do_image_rotation(angle, img, sigma_des, logger=None): + if logger is None: + logger = getLogger(__package__) img_rot = rotate_image(img, angle) img_rot[img_rot!=0] = 1 try: var = find_num_col_deskew(img_rot, sigma_des, 20.3) except: + logger.exception("cannot determine variance for angle %.2f°", angle) var = 0 return var -def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=False, plotter=None): - num_cores = cpu_count() +def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, + main_page=False, logger=None, plotter=None, map=map): if main_page and plotter: plotter.save_plot_of_textline_density(img_patch_org) @@ -1615,16 +1619,16 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals #plt.imshow(img_resized) #plt.show() angles = np.array([-45, 0, 45, 90,]) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) angles = np.linspace(angle - 22.5, angle + 22.5, n_tot_angles) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) elif main_page: #plt.imshow(img_resized) #plt.show() angles = np.linspace(-12, 12, n_tot_angles)#np.array([0 , 45 , 90 , -45]) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) early_slope_edge=11 if abs(angle) > early_slope_edge: @@ -1632,11 +1636,11 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals angles = np.linspace(-90, -12, n_tot_angles) else: angles = np.linspace(90, 12, n_tot_angles) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) else: angles = np.linspace(-25, 25, int(0.5 * n_tot_angles) + 10) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) early_slope_edge=22 if abs(angle) > early_slope_edge: @@ -1644,30 +1648,35 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, main_page=Fals angles = np.linspace(-90, -25, int(0.5 * n_tot_angles) + 10) else: angles = np.linspace(90, 25, int(0.5 * n_tot_angles) + 10) - angle = get_smallest_skew(img_resized, sigma_des, angles, num_cores=num_cores, plotter=plotter) + angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) return angle -def get_smallest_skew(img, sigma_des, angles, num_cores=1, plotter=None): - with Pool(processes=num_cores) as pool: - results = pool.map(partial(do_image_rotation, img=img, sigma_des=sigma_des), angles) +def get_smallest_skew(img, sigma_des, angles, logger=None, plotter=None, map=map): + if logger is None: + logger = getLogger(__package__) + results = list(map(partial(do_image_rotation, img=img, sigma_des=sigma_des, logger=logger), angles)) if plotter: plotter.save_plot_of_rotation_angle(angles, results) try: var_res = np.array(results) + assert var_res.any() angle = angles[np.argmax(var_res)] except: + logger.exception("cannot determine best angle among %s", str(angles)) angle = 0 return angle def do_work_of_slopes_new( box_text, contour, contour_par, index_r_con, textline_mask_tot_ea, image_page_rotated, slope_deskew, - logger, MAX_SLOPE=999, KERNEL=None, plotter=None + logger=None, MAX_SLOPE=999, KERNEL=None, plotter=None ): - logger.debug('enter do_work_of_slopes_new') if KERNEL is None: KERNEL = np.ones((5, 5), np.uint8) + if logger is None: + logger = getLogger(__package__) + logger.debug('enter do_work_of_slopes_new') x, y, w, h = box_text _, crop_coor = crop_image_inside_box(box_text, image_page_rotated) @@ -1693,11 +1702,11 @@ def do_work_of_slopes_new( else: sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) img_int_p[img_int_p > 0] = 1 - slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=plotter) + slope_for_all = return_deskew_slop(img_int_p, sigma_des, logger=logger, plotter=plotter) if abs(slope_for_all) <= 0.5: slope_for_all = slope_deskew - except Exception as why: - logger.error(why) + except: + logger.exception("cannot determine angle of contours") slope_for_all = MAX_SLOPE if slope_for_all == MAX_SLOPE: @@ -1728,11 +1737,13 @@ def do_work_of_slopes_new( def do_work_of_slopes_new_curved( box_text, contour, contour_par, index_r_con, textline_mask_tot_ea, image_page_rotated, mask_texts_only, num_col, scale_par, slope_deskew, - logger, MAX_SLOPE=999, KERNEL=None, plotter=None + logger=None, MAX_SLOPE=999, KERNEL=None, plotter=None ): - logger.debug("enter do_work_of_slopes_new_curved") if KERNEL is None: KERNEL = np.ones((5, 5), np.uint8) + if logger is None: + logger = getLogger(__package__) + logger.debug("enter do_work_of_slopes_new_curved") x, y, w, h = box_text all_text_region_raw = textline_mask_tot_ea[y: y + h, x: x + w].astype(np.uint8) @@ -1755,11 +1766,11 @@ def do_work_of_slopes_new_curved( else: sigma_des = max(1, int(y_diff_mean * (4.0 / 40.0))) img_int_p[img_int_p > 0] = 1 - slope_for_all = return_deskew_slop(img_int_p, sigma_des, plotter=plotter) + slope_for_all = return_deskew_slop(img_int_p, sigma_des, logger=logger, plotter=plotter) if abs(slope_for_all) < 0.5: slope_for_all = slope_deskew - except Exception as why: - logger.error(why) + except: + logger.exception("cannot determine angle of contours") slope_for_all = MAX_SLOPE if slope_for_all == MAX_SLOPE: @@ -1778,7 +1789,7 @@ def do_work_of_slopes_new_curved( # print(slope_for_all,'slope_for_all') textline_rotated_separated = separate_lines_new2(textline_biggest_region[y: y+h, x: x+w], 0, num_col, slope_for_all, - plotter=plotter) + logger=logger, plotter=plotter) # new line added ##print(np.shape(textline_rotated_separated),np.shape(mask_biggest)) @@ -1818,8 +1829,10 @@ def do_work_of_slopes_new_curved( def do_work_of_slopes_new_light( box_text, contour, contour_par, index_r_con, textline_mask_tot_ea, image_page_rotated, slope_deskew, - logger + logger=None ): + if logger is None: + logger = getLogger(__package__) logger.debug('enter do_work_of_slopes_new_light') x, y, w, h = box_text From 3b70b11ea6e18b00e70ec0169ebe4963d91fd364 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 18:36:20 +0000 Subject: [PATCH 304/412] avoid deskewing patches if binary-empty --- src/eynollah/utils/separate_lines.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index 48e1c5b..788a510 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1515,9 +1515,9 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, logger=None, pl # img_patch = img[index_y_d:index_y_u, index_x_d:index_x_u, :] img_xline = img_patch_ineterst[:, index_x_d:index_x_u] - sigma = 2 try: - slope_xline = return_deskew_slop(img_xline, sigma, plotter=plotter) + assert img_xline.any() + slope_xline = return_deskew_slop(img_xline, 2, logger=logger, plotter=plotter) except: slope_xline = 0 From 9270ea4550a39a07699338693962c6dedf146097 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 18:37:20 +0000 Subject: [PATCH 305/412] annotate region angles in PAGE --- src/eynollah/writer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index 496b3db..5f282f2 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -60,6 +60,7 @@ class EynollahXmlWriter(): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) marginal_region.add_TextLine(textline) + marginal_region.set_orientation(slopes_marginals[marginal_idx]) points_co = '' for l in range(len(all_found_textline_polygons_marginals[marginal_idx][j])): if not (self.curved_line or self.textline_light): @@ -102,6 +103,7 @@ class EynollahXmlWriter(): if ocr_all_textlines_textregion: textline.set_TextEquiv( [ TextEquivType(Unicode=ocr_all_textlines_textregion[j]) ] ) text_region.add_TextLine(textline) + text_region.set_orientation(slopes[region_idx]) region_bboxes = all_box_coord[region_idx] points_co = '' for idx_contour_textline, contour_textline in enumerate(all_found_textline_polygons[region_idx][j]): From b9ca7a6191f672c4b9f0da0179ff0d49cc3d63be Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 18:44:54 +0000 Subject: [PATCH 306/412] log num_cols-dependent resizing --- src/eynollah/eynollah.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 8c92b92..8b8808c 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1861,6 +1861,8 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: + self.logger.debug("resized to %dx%d for %d cols", + img_resized.shape[1], img_resized.shape[0], num_col_classifier) prediction_regions_org = self.do_prediction_new_concept( True, img_resized, self.model_region_1_2, n_batch_inference=1, thresholding_for_some_classes_in_light_version=True) @@ -1873,6 +1875,8 @@ class Eynollah: else: new_h = (900+ (num_col_classifier-3)*100) img_resized = resize_image(img_bin, int(new_h * img_bin.shape[0] /img_bin.shape[1]), new_h) + self.logger.debug("resized to %dx%d (new_h=%d) for %d cols", + img_resized.shape[1], img_resized.shape[0], new_h, num_col_classifier) prediction_regions_org = self.do_prediction_new_concept( True, img_resized, self.model_region_1_2, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) From b4b0890294d2dc1fbf6ca84794587d5185a7546f Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 18:45:18 +0000 Subject: [PATCH 307/412] add option to overwrite output xml, but skip by default if file exists --- src/eynollah/cli.py | 9 ++++++++- src/eynollah/eynollah.py | 13 +++++++++++-- src/eynollah/writer.py | 6 +++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index 5f4b5a4..a9b5765 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -97,6 +97,12 @@ def binarization(patches, model_dir, input_image, output_image, dir_in, dir_out) type=click.Path(exists=True, file_okay=False), required=True, ) +@click.option( + "--overwrite", + "-O", + help="overwrite (instead of skipping) if output xml exists", + is_flag=True, +) @click.option( "--dir_in", "-di", @@ -253,7 +259,7 @@ def binarization(patches, model_dir, input_image, output_image, dir_in, dir_out) help="Override log level globally to this", ) -def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, save_all, extract_only_images, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, skip_layout_and_reading_order, ignore_page_extraction, log_level): +def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_deskewed, save_all, extract_only_images, save_page, enable_plotting, allow_enhancement, curved_line, textline_light, full_layout, tables, right2left, input_binary, allow_scaling, headers_off, light_version, reading_order_machine_based, do_ocr, num_col_upper, num_col_lower, skip_layout_and_reading_order, ignore_page_extraction, log_level): initLogging() if log_level: getLogger('eynollah').setLevel(getLevelName(log_level)) @@ -273,6 +279,7 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s sys.exit(1) eynollah = Eynollah( image_filename=image, + overwrite=overwrite, dir_out=out, dir_in=dir_in, dir_models=model, diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 8b8808c..8883f19 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -165,6 +165,7 @@ class Eynollah: image_filename=None, image_pil=None, image_filename_stem=None, + overwrite=False, dir_out=None, dir_in=None, dir_of_cropped_images=None, @@ -203,6 +204,7 @@ class Eynollah: if override_dpi: self.dpi = override_dpi self.image_filename = image_filename + self.overwrite = overwrite self.dir_out = dir_out self.dir_in = dir_in self.dir_of_all = dir_of_all @@ -360,6 +362,7 @@ class Eynollah: curved_line=self.curved_line, textline_light = self.textline_light, pcgts=self.pcgts) + def imread(self, grayscale=False, uint8=True): key = 'img' if grayscale: @@ -4460,8 +4463,14 @@ class Eynollah: if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) #print("text region early -11 in %.1fs", time.time() - t0) - - + + if os.path.exists(self.writer.output_filename): + if self.overwrite: + self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename) + else: + self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) + continue + if self.extract_only_images: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index 5f282f2..dc5a5dc 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -28,6 +28,7 @@ class EynollahXmlWriter(): self.counter = EynollahIdCounter() self.dir_out = dir_out self.image_filename = image_filename + self.output_filename = os.path.join(self.dir_out, self.image_filename_stem) + ".xml" self.curved_line = curved_line self.textline_light = textline_light self.pcgts = pcgts @@ -163,9 +164,8 @@ class EynollahXmlWriter(): coords.set_points(points_co[:-1]) def write_pagexml(self, pcgts): - out_fname = os.path.join(self.dir_out, self.image_filename_stem) + ".xml" - self.logger.info("output filename: '%s'", out_fname) - with open(out_fname, 'w') as f: + self.logger.info("output filename: '%s'", self.output_filename) + with open(self.output_filename, 'w') as f: f.write(to_xml(pcgts)) def build_pagexml_no_full_layout(self, found_polygons_text_region, page_coord, order_of_texts, id_of_texts, all_found_textline_polygons, all_box_coord, found_polygons_text_region_img, found_polygons_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_to_be_written_in_xml, found_polygons_tables, ocr_all_textlines): From dcaf79628371d03e2eed790c792930ba30079545 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 23:07:56 +0000 Subject: [PATCH 308/412] change polarity of orientation angle (PAGE schema required cw=positive) --- src/eynollah/writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index dc5a5dc..66747b1 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -61,7 +61,7 @@ class EynollahXmlWriter(): coords = CoordsType() textline = TextLineType(id=counter.next_line_id, Coords=coords) marginal_region.add_TextLine(textline) - marginal_region.set_orientation(slopes_marginals[marginal_idx]) + marginal_region.set_orientation(-slopes_marginals[marginal_idx]) points_co = '' for l in range(len(all_found_textline_polygons_marginals[marginal_idx][j])): if not (self.curved_line or self.textline_light): @@ -104,7 +104,7 @@ class EynollahXmlWriter(): if ocr_all_textlines_textregion: textline.set_TextEquiv( [ TextEquivType(Unicode=ocr_all_textlines_textregion[j]) ] ) text_region.add_TextLine(textline) - text_region.set_orientation(slopes[region_idx]) + text_region.set_orientation(-slopes[region_idx]) region_bboxes = all_box_coord[region_idx] points_co = '' for idx_contour_textline, contour_textline in enumerate(all_found_textline_polygons[region_idx][j]): From e9c0d716f62659466c66ae9c8b634cd22634e181 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Wed, 11 Dec 2024 23:48:56 +0000 Subject: [PATCH 309/412] CI: install optional dependencies, too --- .github/workflows/test-eynollah.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 8a6941f..479c371 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -36,7 +36,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install . + pip install .[OCR,plotting] pip install -r requirements-test.txt - name: Test with pytest run: make test From 0e8c561618ce267259f8e9b354f151e4b2fd040d Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 14 Dec 2024 00:24:29 +0100 Subject: [PATCH 310/412] debugging issues --- src/eynollah/eynollah.py | 837 ++++++++++++++------------- src/eynollah/utils/separate_lines.py | 6 +- 2 files changed, 422 insertions(+), 421 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 8883f19..443b5e9 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -195,6 +195,8 @@ class Eynollah: logger=None, pcgts=None, ): + if skip_layout_and_reading_order: + textline_light = True self.light_version = light_version if not dir_in: if image_pil: @@ -1512,7 +1514,7 @@ class Eynollah: textlines_ins = [polygons_of_textlines[ind] for ind in indexes_in] all_found_textline_polygons.append(textlines_ins) - slopes.append(0) + slopes.append(slope_deskew) _, crop_coor = crop_image_inside_box(boxes[index],image_page_rotated) @@ -1527,11 +1529,8 @@ class Eynollah: results = self.executor.map(partial(do_work_of_slopes_new_light, textline_mask_tot_ea=textline_mask_tot, image_page_rotated=image_page_rotated, - slope_deskew=slope_deskew, - MAX_SLOPE=MAX_SLOPE, - KERNEL=KERNEL, - logger=self.logger, - plotter=self.plotter,), + slope_deskew=slope_deskew,textline_light=self.textline_light, + logger=self.logger,), boxes, contours, contours_par, range(len(contours_par))) #textline_polygons, boxes, text_regions, text_regions_par, box_coord, index_text_con, slopes = zip(*results) self.logger.debug("exit get_slopes_and_deskew_new_light") @@ -4245,7 +4244,7 @@ class Eynollah: - def filter_contours_without_textline_inside(self,contours,text_con_org, contours_textline): + def filter_contours_without_textline_inside(self,contours,text_con_org, contours_textline, contours_only_text_parent_d_ordered): ###contours_txtline_of_all_textregions = [] @@ -4282,8 +4281,9 @@ class Eynollah: contours.pop(ind_u_a_trs) contours_textline.pop(ind_u_a_trs) text_con_org.pop(ind_u_a_trs) + contours_only_text_parent_d_ordered.pop(ind_u_a_trs) - return contours, text_con_org, contours_textline + return contours, text_con_org, contours_textline, contours_only_text_parent_d_ordered, np.array(range(len(contours))) def dilate_textlines(self,all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): @@ -4470,7 +4470,7 @@ class Eynollah: else: self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) continue - + if self.extract_only_images: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) @@ -4487,12 +4487,9 @@ class Eynollah: continue else: return pcgts - - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) - #print("text region early -1 in %.1fs", time.time() - t0) - t1 = time.time() if self.skip_layout_and_reading_order: + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) @@ -4522,467 +4519,471 @@ class Eynollah: polygons_lines_xml = [] contours_tables = [] ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - if self.dir_in: - continue - else: - return pcgts - - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) - - if num_col_classifier == 1 or num_col_classifier ==2: - if num_col_classifier == 1: - img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) - else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - - t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - #plt.imshow(table_prediction) - #plt.show() - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) if self.dir_in: self.writer.write_pagexml(pcgts) continue else: return pcgts - - #print("text region early in %.1fs", time.time() - t0) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) + if not self.extract_only_images and not self.skip_layout_and_reading_order: + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) + #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - elif num_col_classifier in (1,2): - org_h_l_m = textline_mask_tot_ea.shape[0] - org_w_l_m = textline_mask_tot_ea.shape[1] - if num_col_classifier == 1: - img_w_new = 2000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: - img_w_new = 2400 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - image_page = resize_image(image_page,img_h_new, img_w_new ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - mask_images = resize_image(mask_images,img_h_new, img_w_new ) - mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) - text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) - table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) - - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - - if self.light_version and num_col_classifier in (1,2): - image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) - text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) - textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) - text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) - table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) - image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) - - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) - ## birdan sora chock chakir - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ - self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - else: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ - self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.light_version: - drop_label_in_full_layout = 4 - textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) + + if num_col_classifier == 1 or num_col_classifier ==2: + if num_col_classifier == 1: + img_w_new = 1000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 1300 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + else: + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) + #plt.imshow(table_prediction) + #plt.show() + + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + + #print("text region early in %.1fs", time.time() - t0) + t1 = time.time() + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + elif num_col_classifier in (1,2): + org_h_l_m = textline_mask_tot_ea.shape[0] + org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1: + img_w_new = 2000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 2400 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + image_page = resize_image(image_page,img_h_new, img_w_new ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + mask_images = resize_image(mask_images,img_h_new, img_w_new ) + mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) + text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) + table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) + + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + + if self.light_version and num_col_classifier in (1,2): + image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) + text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) + textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) + text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) + table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) + image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) + + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) + ## birdan sora chock chakir + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ + self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + else: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ + self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + if self.light_version: + drop_label_in_full_layout = 4 + textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 - text_only = ((img_revised_tab[:, :] == 1)) * 1 - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - - #print("text region early 2 in %.1fs", time.time() - t0) - ###min_con_area = 0.000005 - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) - - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - + text_only = ((img_revised_tab[:, :] == 1)) * 1 if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + #print("text region early 2 in %.1fs", time.time() - t0) + ###min_con_area = 0.000005 + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) - #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) - #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) + + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) + + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) + + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) + + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) + + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() + else: + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] + if not len(contours_only_text_parent): + # stop early + empty_marginals = [[]] * len(polygons_of_marginals) + if self.full_layout: + pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) + else: + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) + self.logger.info("Job done in %.1fs", time.time() - t0) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts - if not len(contours_only_text_parent): - # stop early - empty_marginals = [[]] * len(polygons_of_marginals) - if self.full_layout: - pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) - else: - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) - self.logger.info("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - - #print("text region early 3 in %.1fs", time.time() - t0) - if self.light_version: - contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) - #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) - #txt_con_org = self.dilate_textregions_contours(txt_con_org) - #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) - ## birdan sora chock chakir - if not self.curved_line: + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: - if self.textline_light: - #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + #print("text region early 3.5 in %.1fs", time.time() - t0) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) + #txt_con_org = self.dilate_textregions_contours(txt_con_org) + #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) + #print("text region early 5 in %.1fs", time.time() - t0) + ## birdan sora chock chakir + if not self.curved_line: + if self.light_version: + if self.textline_light: + #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ + self.get_slopes_and_deskew_new_light2(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ - # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ + # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ - # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) - #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) - #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ + # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) - contours_only_text_parent, txt_con_org, all_found_textline_polygons = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons) + contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, index_by_text_par_con = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered) + else: + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ + self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) else: - textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) + scale_param = 1 + textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - else: - scale_param = 1 - textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - fun = check_any_text_region_in_model_one_is_main_or_header_light - else: - fun = check_any_text_region_in_model_one_is_main_or_header - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ - all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ - contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ - fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - - if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, - all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, - kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) - pixel_lines = 6 - - if not self.reading_order_machine_based: - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - - if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() - - if self.full_layout: - - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - - if self.ocr: - ocr_all_textlines = [] - else: - ocr_all_textlines = None - - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, - all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, - cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - - else: - contours_only_text_parent_h = None - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: + #print("text region early 6 in %.1fs", time.time() - t0) + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - #except: #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + fun = check_any_text_region_in_model_one_is_main_or_header_light + else: + fun = check_any_text_region_in_model_one_is_main_or_header + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ + all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ + contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ + fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + if self.plotter: + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - if self.ocr: + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, + all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, + kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) + pixel_lines = 6 - device = cuda.get_current_device() - device.reset() - gc.collect() - model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") - torch.cuda.empty_cache() - model_ocr.to(device) - - ind_tot = 0 - #cv2.imwrite('./img_out.png', image_page) - - ocr_all_textlines = [] - for indexing, ind_poly_first in enumerate(all_found_textline_polygons): - ocr_textline_in_textregion = [] - for indexing2, ind_poly in enumerate(ind_poly_first): - if not (self.textline_light or self.curved_line): - ind_poly = copy.deepcopy(ind_poly) - box_ind = all_box_coord[indexing] - #print(ind_poly,np.shape(ind_poly), 'ind_poly') - #print(box_ind) - ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) - #print(ind_poly_copy) - ind_poly[ind_poly<0] = 0 - x, y, w, h = cv2.boundingRect(ind_poly) - #print(ind_poly_copy, np.shape(ind_poly_copy)) - #print(x, y, w, h, h/float(w),'ratio') - h2w_ratio = h/float(w) - mask_poly = np.zeros(image_page.shape) - if not self.light_version: - img_poly_on_img = np.copy(image_page) + if not self.reading_order_machine_based: + if not self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) else: - img_poly_on_img = np.copy(img_bin_light) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - if self.textline_light: - mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - img_croped = img_poly_on_img[y:y+h, x:x+w, :] - #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) - text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) - ocr_textline_in_textregion.append(text_ocr) + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + if self.full_layout: - ind_tot = ind_tot +1 - ocr_all_textlines.append(ocr_textline_in_textregion) + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, + cont_page, polygons_lines_xml, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + print("Job done in %.1fs", time.time() - t0) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts else: - ocr_all_textlines = None - #print(ocr_all_textlines) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, - cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) + contours_only_text_parent_h = None + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - if self.dir_in: - self.writer.write_pagexml(pcgts) - self.logger.info("Job done in %.1fs", time.time() - t0) - #print("Job done in %.1fs" % (time.time() - t0)) + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) + + if self.dir_in: + self.writer.write_pagexml(pcgts) + self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs" % (time.time() - t0)) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index 788a510..f037a9f 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -1828,7 +1828,7 @@ def do_work_of_slopes_new_curved( def do_work_of_slopes_new_light( box_text, contour, contour_par, index_r_con, - textline_mask_tot_ea, image_page_rotated, slope_deskew, + textline_mask_tot_ea, image_page_rotated, slope_deskew, textline_light, logger=None ): if logger is None: @@ -1845,7 +1845,7 @@ def do_work_of_slopes_new_light( mask_only_con_region = np.zeros(textline_mask_tot_ea.shape) mask_only_con_region = cv2.fillPoly(mask_only_con_region, pts=[contour_par], color=(1, 1, 1)) - if self.textline_light: + if textline_light: all_text_region_raw = np.copy(textline_mask_tot_ea) all_text_region_raw[mask_only_con_region == 0] = 0 cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(all_text_region_raw) @@ -1857,4 +1857,4 @@ def do_work_of_slopes_new_light( all_text_region_raw[mask_only_con_region == 0] = 0 cnt_clean_rot = textline_contours_postprocessing(all_text_region_raw, slope_deskew, contour_par, box_text) - return cnt_clean_rot, box_text, contour, contour_par, crop_coor, index_r_con, slope + return cnt_clean_rot, box_text, contour, contour_par, crop_coor, index_r_con, slope_deskew From f93c6c288d9525202957da5bb000202a657e6df8 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sat, 14 Dec 2024 02:50:17 +0100 Subject: [PATCH 311/412] function of patch-wise inference with scatter_nd is added --- src/eynollah/eynollah.py | 107 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 443b5e9..28cb330 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1047,6 +1047,110 @@ class Eynollah: #label_scaled_padded[h_start:h_start+h_n, w_start:w_start+w_n,:] = label_res[:,:,:] return img_scaled_padded#, label_scaled_padded + def do_prediction_new_concept_scatter_nd(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): + self.logger.debug("enter do_prediction_new_concept") + + img_height_model = model.layers[-1].output_shape[1] + img_width_model = model.layers[-1].output_shape[2] + + if not patches: + img_h_page = img.shape[0] + img_w_page = img.shape[1] + img = img / 255.0 + img = resize_image(img, img_height_model, img_width_model) + + label_p_pred = model.predict(img[np.newaxis], verbose=0) + seg = np.argmax(label_p_pred, axis=3)[0] + + if thresholding_for_artificial_class_in_light_version: + #seg_text = label_p_pred[0,:,:,1] + #seg_text[seg_text<0.2] =0 + #seg_text[seg_text>0] =1 + #seg[seg_text==1]=1 + + seg_art = label_p_pred[0,:,:,4] + seg_art[seg_art<0.2] =0 + seg_art[seg_art>0] =1 + seg[seg_art==1]=4 + + + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + prediction_true = resize_image(seg_color, img_h_page, img_w_page) + prediction_true = prediction_true.astype(np.uint8) + return prediction_true + + if img.shape[0] < img_height_model: + img = resize_image(img, img_height_model, img.shape[1]) + + if img.shape[1] < img_width_model: + img = resize_image(img, img.shape[0], img_width_model) + + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) + ##margin = int(marginal_of_patch_percent * img_height_model) + #width_mid = img_width_model - 2 * margin + #height_mid = img_height_model - 2 * margin + img = img / float(255.0) + + img = img.astype(np.float16) + img_h = img.shape[0] + img_w = img.shape[1] + + stride_x = img_width_model - 100 + stride_y = img_height_model - 100 + + one_tensor = tf.ones_like(img) + img_patches = tf.image.extract_patches(images=[img,one_tensor], + sizes=[1, img_height_model, img_width_model, 1], + strides=[1, stride_y, stride_x, 1], + rates=[1, 1, 1, 1], + padding='SAME') + + one_patches = img_patches[1] + img_patches = img_patches[0] + img_patches = tf.squeeze(img_patches) + + img_patches_resh = tf.reshape(img_patches, shape = (img_patches.shape[0]*img_patches.shape[1], img_height_model, img_width_model, 3)) + + pred_patches = model.predict(img_patches_resh, batch_size=n_batch_inference) + + one_patches = tf.squeeze(one_patches) + one_patches = tf.reshape(one_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model,img_width_model,3]) + + x = tf.range(img.shape[1]) + y = tf.range(img.shape[0]) + x, y = tf.meshgrid(x, y) + indices = tf.stack([y, x], axis=-1) + + indices_patches = tf.image.extract_patches(images=tf.expand_dims(indices, axis=0), sizes=[1, img_height_model, img_width_model, 1], strides=[1, stride_y, stride_x, 1], rates=[1, 1, 1, 1], padding='SAME') + indices_patches = tf.squeeze(indices_patches) + indices_patches = tf.reshape(indices_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model, img_width_model,2]) + + margin_y = int( (img_height_model - stride_y)/2. ) + margin_x = int( (img_width_model - stride_x)/2. ) + + mask_margin = np.zeros((img_height_model, img_width_model)) + + mask_margin[margin_y:img_height_model-margin_y, margin_x:img_width_model-margin_x] = 1 + + indices_patches_array = indices_patches.numpy() + + for i in range(indices_patches_array.shape[0]): + indices_patches_array[i,:,:,0] = indices_patches_array[i,:,:,0]*mask_margin + indices_patches_array[i,:,:,1] = indices_patches_array[i,:,:,1]*mask_margin + + reconstructed = tf.scatter_nd(indices=indices_patches_array, updates=pred_patches, shape=(img.shape[0],img.shape[1],pred_patches.shape[-1])) + reconstructed_argmax = reconstructed.numpy() + + prediction_true = np.argmax(reconstructed_argmax, axis=2) + prediction_true = prediction_true.astype(np.uint8) + + gc.collect() + return np.repeat(prediction_true[:, :, np.newaxis], 3, axis=2) + + + + + def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction_new_concept") @@ -4891,7 +4995,7 @@ class Eynollah: all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) - print("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs", time.time() - t0) if self.dir_in: self.writer.write_pagexml(pcgts) continue @@ -4975,6 +5079,7 @@ class Eynollah: pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + #print("Job done in %.1fs" % (time.time() - t0)) self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: return pcgts From 0ae28f7d3ef33d6bf3650b99bc7646c3234341d1 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sat, 14 Dec 2024 12:15:56 +0000 Subject: [PATCH 312/412] switch from stdlib to loky.ProcessPoolExecutor, ensure shutdown --- requirements.txt | 1 + src/eynollah/eynollah.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d72df29..ef3fe31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ scikit-learn >= 0.23.2 tensorflow < 2.13 imutils >= 0.5.3 numba <= 0.58.1 +loky diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 28cb330..8139b11 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -10,11 +10,12 @@ import math import os import sys import time +import atexit import warnings from functools import partial from pathlib import Path from multiprocessing import cpu_count -from concurrent.futures import ProcessPoolExecutor +from loky import ProcessPoolExecutor import gc from ocrd_utils import getLogger import cv2 @@ -257,7 +258,8 @@ class Eynollah: pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') # for parallelization of CPU-intensive tasks: - self.executor = ProcessPoolExecutor(max_workers=cpu_count()) + self.executor = ProcessPoolExecutor(max_workers=cpu_count(), timeout=1200) + atexit.register(self.executor.shutdown) self.dir_models = dir_models self.model_dir_of_enhancement = dir_models + "/eynollah-enhancement_20210425" self.model_dir_of_binarization = dir_models + "/eynollah-binarization_20210425" From fbeef79d50412ca15d71766af9c109ee6a16aa10 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 16 Dec 2024 01:11:54 +0100 Subject: [PATCH 313/412] adding scatter_nd inference --- src/eynollah/eynollah.py | 109 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index e802e29..006bfea 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1151,6 +1151,107 @@ class Eynollah: #label_scaled_padded[h_start:h_start+h_n, w_start:w_start+w_n,:] = label_res[:,:,:] return img_scaled_padded#, label_scaled_padded + + def do_prediction_new_concept_scatter_nd(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): + self.logger.debug("enter do_prediction_new_concept") + + img_height_model = model.layers[-1].output_shape[1] + img_width_model = model.layers[-1].output_shape[2] + + if not patches: + img_h_page = img.shape[0] + img_w_page = img.shape[1] + img = img / 255.0 + img = resize_image(img, img_height_model, img_width_model) + + label_p_pred = model.predict(img[np.newaxis], verbose=0) + seg = np.argmax(label_p_pred, axis=3)[0] + + if thresholding_for_artificial_class_in_light_version: + #seg_text = label_p_pred[0,:,:,1] + #seg_text[seg_text<0.2] =0 + #seg_text[seg_text>0] =1 + #seg[seg_text==1]=1 + + seg_art = label_p_pred[0,:,:,4] + seg_art[seg_art<0.2] =0 + seg_art[seg_art>0] =1 + seg[seg_art==1]=4 + + + seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) + prediction_true = resize_image(seg_color, img_h_page, img_w_page) + prediction_true = prediction_true.astype(np.uint8) + return prediction_true + + if img.shape[0] < img_height_model: + img = resize_image(img, img_height_model, img.shape[1]) + + if img.shape[1] < img_width_model: + img = resize_image(img, img.shape[0], img_width_model) + + self.logger.debug("Patch size: %sx%s", img_height_model, img_width_model) + ##margin = int(marginal_of_patch_percent * img_height_model) + #width_mid = img_width_model - 2 * margin + #height_mid = img_height_model - 2 * margin + img = img / float(255.0) + + img = img.astype(np.float16) + img_h = img.shape[0] + img_w = img.shape[1] + + stride_x = img_width_model - 100 + stride_y = img_height_model - 100 + + one_tensor = tf.ones_like(img) + img_patches = tf.image.extract_patches(images=[img,one_tensor], + sizes=[1, img_height_model, img_width_model, 1], + strides=[1, stride_y, stride_x, 1], + rates=[1, 1, 1, 1], + padding='SAME') + + one_patches = img_patches[1] + img_patches = img_patches[0] + img_patches = tf.squeeze(img_patches) + + img_patches_resh = tf.reshape(img_patches, shape = (img_patches.shape[0]*img_patches.shape[1], img_height_model, img_width_model, 3)) + + pred_patches = model.predict(img_patches_resh, batch_size=n_batch_inference) + + one_patches = tf.squeeze(one_patches) + one_patches = tf.reshape(one_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model,img_width_model,3]) + + x = tf.range(img.shape[1]) + y = tf.range(img.shape[0]) + x, y = tf.meshgrid(x, y) + indices = tf.stack([y, x], axis=-1) + + indices_patches = tf.image.extract_patches(images=tf.expand_dims(indices, axis=0), sizes=[1, img_height_model, img_width_model, 1], strides=[1, stride_y, stride_x, 1], rates=[1, 1, 1, 1], padding='SAME') + indices_patches = tf.squeeze(indices_patches) + indices_patches = tf.reshape(indices_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model, img_width_model,2]) + + margin_y = int( (img_height_model - stride_y)/2. ) + margin_x = int( (img_width_model - stride_x)/2. ) + + mask_margin = np.zeros((img_height_model, img_width_model)) + + mask_margin[margin_y:img_height_model-margin_y, margin_x:img_width_model-margin_x] = 1 + + indices_patches_array = indices_patches.numpy() + + for i in range(indices_patches_array.shape[0]): + indices_patches_array[i,:,:,0] = indices_patches_array[i,:,:,0]*mask_margin + indices_patches_array[i,:,:,1] = indices_patches_array[i,:,:,1]*mask_margin + + reconstructed = tf.scatter_nd(indices=indices_patches_array, updates=pred_patches, shape=(img.shape[0],img.shape[1],pred_patches.shape[-1])) + reconstructed_argmax = reconstructed.numpy() + + prediction_true = np.argmax(reconstructed_argmax, axis=2) + prediction_true = prediction_true.astype(np.uint8) + + gc.collect() + return np.repeat(prediction_true[:, :, np.newaxis], 3, axis=2) + def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): self.logger.debug("enter do_prediction") @@ -2089,12 +2190,16 @@ class Eynollah: if not self.dir_in: prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + ##prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, model_textline, n_batch_inference=3) + #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + + ###prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, self.model_textline, n_batch_inference=3) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) @@ -2374,14 +2479,17 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: + ##prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) else: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) + ##prediction_regions_page = self.do_prediction_new_concept_scatter_nd(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) + ###prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: @@ -5501,3 +5609,4 @@ class Eynollah: if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) + print("all Job done in %.1fs", time.time() - t0_tot) From 92bfac4b415520f9627ce06721179316998e7ce9 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 20 Dec 2024 15:47:21 +0100 Subject: [PATCH 314/412] Provide OCR as an option to process a directory of XML files, incorporating layout and text line coordinates. --- src/eynollah/cli.py | 56 +++++- src/eynollah/eynollah.py | 414 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 459 insertions(+), 11 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index bed0c03..4bf5257 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -1,7 +1,7 @@ import sys import click from ocrd_utils import initLogging, setOverrideLogLevel -from eynollah.eynollah import Eynollah +from eynollah.eynollah import Eynollah, Eynollah_ocr from eynollah.sbb_binarize import SbbBinarizer @click.group() @@ -305,6 +305,60 @@ def layout(image, out, dir_in, model, save_images, save_layout, save_deskewed, s else: pcgts = eynollah.run() eynollah.writer.write_pagexml(pcgts) + + +@main.command() +@click.option( + "--dir_in", + "-di", + help="directory of images", + type=click.Path(exists=True, file_okay=False), +) +@click.option( + "--out", + "-o", + help="directory to write output xml data", + type=click.Path(exists=True, file_okay=False), + required=True, +) +@click.option( + "--dir_xmls", + "-dx", + help="directory of xmls", + type=click.Path(exists=True, file_okay=False), +) +@click.option( + "--model", + "-m", + help="directory of models", + type=click.Path(exists=True, file_okay=False), + required=True, +) +@click.option( + "--tr_ocr", + "-trocr/-notrocr", + is_flag=True, + help="if this parameter set to true, transformer ocr will be applied, otherwise cnn_rnn model.", +) +@click.option( + "--log_level", + "-l", + type=click.Choice(['OFF', 'DEBUG', 'INFO', 'WARN', 'ERROR']), + help="Override log level globally to this", +) + +def ocr(dir_in, out, dir_xmls, model, tr_ocr, log_level): + if log_level: + setOverrideLogLevel(log_level) + initLogging() + eynollah_ocr = Eynollah_ocr( + dir_xmls=dir_xmls, + dir_in=dir_in, + dir_out=out, + dir_models=model, + tr_ocr=tr_ocr, + ) + eynollah_ocr.run() if __name__ == "__main__": main() diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 006bfea..95033e9 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -41,6 +41,9 @@ import matplotlib.pyplot as plt # use tf1 compatibility for keras backend from tensorflow.compat.v1.keras.backend import set_session from tensorflow.keras import layers +import json +import xml.etree.ElementTree as ET +from tensorflow.keras.layers import StringLookup from .utils.contour import ( filter_contours_area_of_image, @@ -2188,18 +2191,18 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) if not self.dir_in: - prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + ###prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - ##prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, model_textline, n_batch_inference=3) + prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, model_textline, n_batch_inference=3) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: - prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + ##prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - ###prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, self.model_textline, n_batch_inference=3) + prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, self.model_textline, n_batch_inference=3) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) @@ -2479,17 +2482,17 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - ##prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) + prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) + ###prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) else: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - ##prediction_regions_page = self.do_prediction_new_concept_scatter_nd(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) - prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + prediction_regions_page = self.do_prediction_new_concept_scatter_nd(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + ##prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) - ###prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) + ###prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) + prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: @@ -5610,3 +5613,394 @@ class Eynollah: if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) print("all Job done in %.1fs", time.time() - t0_tot) + + +class Eynollah_ocr: + def __init__( + self, + dir_models, + dir_xmls=None, + dir_in=None, + dir_out=None, + tr_ocr=False, + logger=None, + ): + self.dir_in = dir_in + self.dir_out = dir_out + self.dir_xmls = dir_xmls + self.dir_models = dir_models + self.tr_ocr = tr_ocr + if tr_ocr: + self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self.model_ocr_dir = dir_models + "/trocr_model_ens_of_3_checkpoints_201124" + self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + self.model_ocr.to(self.device) + + else: + self.model_ocr_dir = dir_models + "/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + model_ocr = load_model(self.model_ocr_dir , compile=False) + + self.prediction_model = tf.keras.models.Model( + model_ocr.get_layer(name = "image").input, + model_ocr.get_layer(name = "dense2").output) + + + with open(os.path.join(self.model_ocr_dir, "characters_org.txt"),"r") as config_file: + characters = json.load(config_file) + + + AUTOTUNE = tf.data.AUTOTUNE + + # Mapping characters to integers. + char_to_num = StringLookup(vocabulary=list(characters), mask_token=None) + + # Mapping integers back to original characters. + self.num_to_char = StringLookup( + vocabulary=char_to_num.get_vocabulary(), mask_token=None, invert=True + ) + + def decode_batch_predictions(self, pred, max_len = 128): + # input_len is the product of the batch size and the + # number of time steps. + input_len = np.ones(pred.shape[0]) * pred.shape[1] + + # Decode CTC predictions using greedy search. + # decoded is a tuple with 2 elements. + decoded = tf.keras.backend.ctc_decode(pred, + input_length = input_len, + beam_width = 100) + # The outputs are in the first element of the tuple. + # Additionally, the first element is actually a list, + # therefore we take the first element of that list as well. + #print(decoded,'decoded') + decoded = decoded[0][0][:, :max_len] + + #print(decoded, decoded.shape,'decoded') + + output = [] + for d in decoded: + # Convert the predicted indices to the corresponding chars. + d = tf.strings.reduce_join(self.num_to_char(d)) + d = d.numpy().decode("utf-8") + output.append(d) + return output + + + def distortion_free_resize(self, image, img_size): + w, h = img_size + image = tf.image.resize(image, size=(h, w), preserve_aspect_ratio=True) + + # Check tha amount of padding needed to be done. + pad_height = h - tf.shape(image)[0] + pad_width = w - tf.shape(image)[1] + + # Only necessary if you want to do same amount of padding on both sides. + if pad_height % 2 != 0: + height = pad_height // 2 + pad_height_top = height + 1 + pad_height_bottom = height + else: + pad_height_top = pad_height_bottom = pad_height // 2 + + if pad_width % 2 != 0: + width = pad_width // 2 + pad_width_left = width + 1 + pad_width_right = width + else: + pad_width_left = pad_width_right = pad_width // 2 + + image = tf.pad( + image, + paddings=[ + [pad_height_top, pad_height_bottom], + [pad_width_left, pad_width_right], + [0, 0], + ], + ) + + image = tf.transpose(image, (1, 0, 2)) + image = tf.image.flip_left_right(image) + return image + + def return_start_and_end_of_common_text_of_textline_ocr_without_common_section(self, textline_image): + width = np.shape(textline_image)[1] + height = np.shape(textline_image)[0] + common_window = int(0.06*width) + + width1 = int ( width/2. - common_window ) + width2 = int ( width/2. + common_window ) + + img_sum = np.sum(textline_image[:,:,0], axis=0) + sum_smoothed = gaussian_filter1d(img_sum, 3) + + peaks_real, _ = find_peaks(sum_smoothed, height=0) + + if len(peaks_real)>70: + + peaks_real = peaks_real[(peaks_realwidth1)] + + arg_max = np.argmax(sum_smoothed[peaks_real]) + + peaks_final = peaks_real[arg_max] + + return peaks_final + else: + return None + + def return_textlines_split_if_needed(self, textline_image): + + split_point = self.return_start_and_end_of_common_text_of_textline_ocr_without_common_section(textline_image) + if split_point: + image1 = textline_image[:, :split_point,:]# image.crop((0, 0, width2, height)) + image2 = textline_image[:, split_point:,:]#image.crop((width1, 0, width, height)) + return [image1, image2] + else: + return None + + def run(self): + ls_imgs = os.listdir(self.dir_in) + + if self.tr_ocr: + b_s = 2 + for ind_img in ls_imgs: + t0 = time.time() + file_name = ind_img.split('.')[0] + dir_img = os.path.join(self.dir_in, ind_img) + dir_xml = os.path.join(self.dir_xmls, file_name+'.xml') + out_file_ocr = os.path.join(self.dir_out, file_name+'.xml') + img = cv2.imread(dir_img) + + ##file_name = Path(dir_xmls).stem + tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding = 'iso-8859-5')) + root1=tree1.getroot() + alltags=[elem.tag for elem in root1.iter()] + link=alltags[0].split('}')[0]+'}' + + name_space = alltags[0].split('}')[0] + name_space = name_space.split('{')[1] + + region_tags=np.unique([x for x in alltags if x.endswith('TextRegion')]) + + + + cropped_lines = [] + cropped_lines_region_indexer = [] + cropped_lines_meging_indexing = [] + + indexer_text_region = 0 + for nn in root1.iter(region_tags): + for child_textregion in nn: + if child_textregion.tag.endswith("TextLine"): + + for child_textlines in child_textregion: + if child_textlines.tag.endswith("Coords"): + cropped_lines_region_indexer.append(indexer_text_region) + p_h=child_textlines.attrib['points'].split(' ') + textline_coords = np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) + x,y,w,h = cv2.boundingRect(textline_coords) + + h2w_ratio = h/float(w) + + img_poly_on_img = np.copy(img) + mask_poly = np.zeros(img.shape) + mask_poly = cv2.fillPoly(mask_poly, pts=[textline_coords], color=(1, 1, 1)) + + mask_poly = mask_poly[y:y+h, x:x+w, :] + img_crop = img_poly_on_img[y:y+h, x:x+w, :] + img_crop[mask_poly==0] = 255 + + if h2w_ratio > 0.05: + cropped_lines.append(img_crop) + cropped_lines_meging_indexing.append(0) + else: + splited_images = self.return_textlines_split_if_needed(img_crop) + #print(splited_images) + if splited_images: + cropped_lines.append(splited_images[0]) + cropped_lines_meging_indexing.append(1) + cropped_lines.append(splited_images[1]) + cropped_lines_meging_indexing.append(-1) + else: + cropped_lines.append(img_crop) + cropped_lines_meging_indexing.append(0) + indexer_text_region = indexer_text_region +1 + + + extracted_texts = [] + n_iterations = math.ceil(len(cropped_lines) / b_s) + + for i in range(n_iterations): + if i==(n_iterations-1): + n_start = i*b_s + imgs = cropped_lines[n_start:] + else: + n_start = i*b_s + n_end = (i+1)*b_s + imgs = cropped_lines[n_start:n_end] + pixel_values_merged = self.processor(imgs, return_tensors="pt").pixel_values + generated_ids_merged = self.model_ocr.generate(pixel_values_merged.to(self.device)) + generated_text_merged = self.processor.batch_decode(generated_ids_merged, skip_special_tokens=True) + + extracted_texts = extracted_texts + generated_text_merged + + extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] + + extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] + #print(extracted_texts_merged, len(extracted_texts_merged)) + + unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) + + #print(len(unique_cropped_lines_region_indexer), 'unique_cropped_lines_region_indexer') + text_by_textregion = [] + for ind in unique_cropped_lines_region_indexer: + extracted_texts_merged_un = np.array(extracted_texts_merged)[np.array(cropped_lines_region_indexer)==ind] + + text_by_textregion.append(" ".join(extracted_texts_merged_un)) + + #print(len(text_by_textregion) , indexer_text_region, "text_by_textregion") + + + #print(time.time() - t0 ,'elapsed time') + + + indexer = 0 + indexer_textregion = 0 + for nn in root1.iter(region_tags): + text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') + unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') + + + has_textline = False + for child_textregion in nn: + if child_textregion.tag.endswith("TextLine"): + text_subelement = ET.SubElement(child_textregion, 'TextEquiv') + unicode_textline = ET.SubElement(text_subelement, 'Unicode') + unicode_textline.text = extracted_texts_merged[indexer] + indexer = indexer + 1 + has_textline = True + if has_textline: + unicode_textregion.text = text_by_textregion[indexer_textregion] + indexer_textregion = indexer_textregion + 1 + + + + ET.register_namespace("",name_space) + tree1.write(out_file_ocr,xml_declaration=True,method='xml',encoding="utf8",default_namespace=None) + #print("Job done in %.1fs", time.time() - t0) + else: + max_len = 512 + padding_token = 299 + image_width = max_len * 4 + image_height = 32 + b_s = 8 + + + img_size=(image_width, image_height) + + for ind_img in ls_imgs: + t0 = time.time() + file_name = ind_img.split('.')[0] + dir_img = os.path.join(self.dir_in, ind_img) + dir_xml = os.path.join(self.dir_xmls, file_name+'.xml') + out_file_ocr = os.path.join(self.dir_out, file_name+'.xml') + img = cv2.imread(dir_img) + + tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding = 'iso-8859-5')) + root1=tree1.getroot() + alltags=[elem.tag for elem in root1.iter()] + link=alltags[0].split('}')[0]+'}' + + name_space = alltags[0].split('}')[0] + name_space = name_space.split('{')[1] + + region_tags=np.unique([x for x in alltags if x.endswith('TextRegion')]) + + cropped_lines = [] + cropped_lines_region_indexer = [] + cropped_lines_meging_indexing = [] + + tinl = time.time() + indexer_text_region = 0 + for nn in root1.iter(region_tags): + for child_textregion in nn: + if child_textregion.tag.endswith("TextLine"): + + for child_textlines in child_textregion: + if child_textlines.tag.endswith("Coords"): + cropped_lines_region_indexer.append(indexer_text_region) + p_h=child_textlines.attrib['points'].split(' ') + textline_coords = np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) + x,y,w,h = cv2.boundingRect(textline_coords) + + h2w_ratio = h/float(w) + + img_poly_on_img = np.copy(img) + mask_poly = np.zeros(img.shape) + mask_poly = cv2.fillPoly(mask_poly, pts=[textline_coords], color=(1, 1, 1)) + + mask_poly = mask_poly[y:y+h, x:x+w, :] + img_crop = img_poly_on_img[y:y+h, x:x+w, :] + img_crop[mask_poly==0] = 255 + img_crop = tf.reverse(img_crop,axis=[-1]) + img_crop = self.distortion_free_resize(img_crop, img_size) + img_crop = tf.cast(img_crop, tf.float32) / 255.0 + cropped_lines.append(img_crop) + + indexer_text_region = indexer_text_region +1 + + + extracted_texts = [] + + n_iterations = math.ceil(len(cropped_lines) / b_s) + + for i in range(n_iterations): + if i==(n_iterations-1): + n_start = i*b_s + imgs = cropped_lines[n_start:] + imgs = np.array(imgs) + imgs = imgs.reshape(imgs.shape[0], image_width, image_height, 3) + else: + n_start = i*b_s + n_end = (i+1)*b_s + imgs = cropped_lines[n_start:n_end] + imgs = np.array(imgs).reshape(b_s, image_width, image_height, 3) + + + preds = self.prediction_model.predict(imgs, verbose=0) + pred_texts = self.decode_batch_predictions(preds) + + for ib in range(imgs.shape[0]): + pred_texts_ib = pred_texts[ib].strip("[UNK]") + extracted_texts.append(pred_texts_ib) + + unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) + + text_by_textregion = [] + for ind in unique_cropped_lines_region_indexer: + extracted_texts_merged_un = np.array(extracted_texts)[np.array(cropped_lines_region_indexer)==ind] + + text_by_textregion.append(" ".join(extracted_texts_merged_un)) + + indexer = 0 + indexer_textregion = 0 + for nn in root1.iter(region_tags): + text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') + unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') + + + has_textline = False + for child_textregion in nn: + if child_textregion.tag.endswith("TextLine"): + text_subelement = ET.SubElement(child_textregion, 'TextEquiv') + unicode_textline = ET.SubElement(text_subelement, 'Unicode') + unicode_textline.text = extracted_texts[indexer] + indexer = indexer + 1 + has_textline = True + if has_textline: + unicode_textregion.text = text_by_textregion[indexer_textregion] + indexer_textregion = indexer_textregion + 1 + + ET.register_namespace("",name_space) + tree1.write(out_file_ocr,xml_declaration=True,method='xml',encoding="utf8",default_namespace=None) + #print("Job done in %.1fs", time.time() - t0) From 01376af9055440366fb7effece949e903a7de710 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 22 Dec 2024 13:10:05 +0000 Subject: [PATCH 315/412] do_order_of_regions_with_model: simplify --- src/eynollah/eynollah.py | 312 +++++++++------------------------------ 1 file changed, 66 insertions(+), 246 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 8139b11..651bd17 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -2771,6 +2771,7 @@ class Eynollah: for ijv in range(len(y_min_tab_col1)): image_revised_last[int(y_min_tab_col1[ijv]):int(y_max_tab_col1[ijv]),:,:]=pixel_table return image_revised_last + def do_order_of_regions(self, *args, **kwargs): if self.full_layout: return self.do_order_of_regions_full_layout(*args, **kwargs) @@ -3380,22 +3381,35 @@ class Eynollah: model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) return model + def do_order_of_regions_with_model(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] - - img_poly = np.zeros((y_len,x_len), dtype='uint8') - - unique_pix = np.unique(text_regions_p) - + img_poly = np.zeros((y_len,x_len), dtype='uint8') img_poly[text_regions_p[:,:]==1] = 1 img_poly[text_regions_p[:,:]==2] = 2 img_poly[text_regions_p[:,:]==3] = 4 img_poly[text_regions_p[:,:]==6] = 5 - if not self.dir_in: - self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) + img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') + if contours_only_text_parent_h: + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + for j in range(len(cy_main)): + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 + + co_text_all = contours_only_text_parent + contours_only_text_parent_h + else: + co_text_all = contours_only_text_parent + + if not len(co_text_all): + return [], [] + + labels_con = np.zeros((y_len, x_len, len(co_text_all)), dtype=bool) + for i in range(len(co_text_all)): + img = labels_con[:,:,i].astype(np.uint8) + cv2.fillPoly(img, pts=[co_text_all[i]], color=(1,)) + labels_con[:,:,i] = img height1 =672#448 width1 = 448#224 @@ -3405,261 +3419,67 @@ class Eynollah: height3 =672#448 width3 = 448#224 - - img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') - - if contours_only_text_parent_h: - _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) - for j in range(len(cy_main)): - img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - - co_text_all = contours_only_text_parent + contours_only_text_parent_h - else: - co_text_all = contours_only_text_parent - - - labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') - for i in range(len(co_text_all)): - img_label = np.zeros((y_len,x_len,3),dtype='uint8') - img_label=cv2.fillPoly(img_label, pts =[co_text_all[i]], color=(1,1,1)) - labels_con[:,:,i] = img_label[:,:,0] - - - img3= np.copy(img_poly) - - labels_con = resize_image(labels_con, height1, width1) + labels_con = resize_image(labels_con.astype(np.uint8), height1, width1).astype(bool) img_header_and_sep = resize_image(img_header_and_sep, height1, width1) + img_poly = resize_image(img_poly, height3, width3) - img3= resize_image (img3, height3, width3) - - img3 = img3.astype(np.uint16) - - - order_matrix = np.zeros((labels_con.shape[2], labels_con.shape[2]))-1 - inference_bs = 6 - tot_counter = 1 - batch_counter = 0 - i_indexer = [] - j_indexer =[] - - input_1= np.zeros( (inference_bs, height1, width1,3)) - - tot_iteration = int( ( labels_con.shape[2]*(labels_con.shape[2]-1) )/2. ) - full_bs_ite= tot_iteration//inference_bs - last_bs = tot_iteration % inference_bs - - #print(labels_con.shape[2],"number of regions for reading order") - for i in range(labels_con.shape[2]): - for j in range(labels_con.shape[2]): - if j>i: - img1= np.repeat(labels_con[:,:,i][:, :, np.newaxis], 3, axis=2) - img2 = np.repeat(labels_con[:,:,j][:, :, np.newaxis], 3, axis=2) - - img2[:,:,0][img3[:,:]==5] = 2 - img2[:,:,0][img_header_and_sep[:,:]==1] = 3 - - img1[:,:,0][img3[:,:]==5] = 2 - img1[:,:,0][img_header_and_sep[:,:]==1] = 3 - - - i_indexer.append(i) - j_indexer.append(j) - - input_1[batch_counter,:,:,0] = img1[:,:,0]/3. - input_1[batch_counter,:,:,2] = img2[:,:,0]/3. - input_1[batch_counter,:,:,1] = img3[:,:]/5. - - batch_counter = batch_counter+1 - - if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): - y_pr = self.model_reading_order.predict(input_1 , verbose=0) - - if batch_counter==inference_bs: - iteration_batches = inference_bs - else: - iteration_batches = last_bs - for jb in range(iteration_batches): - if y_pr[jb][0]>=0.5: - order_class = 1 - else: - order_class = 0 - - order_matrix[i_indexer[jb],j_indexer[jb]] = y_pr[jb][0]#order_class - order_matrix[j_indexer[jb],i_indexer[jb]] = 1-y_pr[jb][0]#int( 1 - order_class) - - batch_counter = 0 - - i_indexer = [] - j_indexer = [] - tot_counter = tot_counter+1 - - - sum_mat = np.sum(order_matrix, axis=1) - index_sort = np.argsort(sum_mat) - index_sort = index_sort[::-1] - - REGION_ID_TEMPLATE = 'region_%04d' - order_of_texts = [] - id_of_texts = [] - for order, id_text in enumerate(index_sort): - order_of_texts.append(id_text) - id_of_texts.append( REGION_ID_TEMPLATE % order ) - - - return order_of_texts, id_of_texts - - def update_list_and_return_first_with_length_bigger_than_one(self,index_element_to_be_updated, innner_index_pr_pos, pr_list, pos_list,list_inp): - list_inp.pop(index_element_to_be_updated) - if len(pr_list)>0: - list_inp.insert(index_element_to_be_updated, pr_list) - else: - index_element_to_be_updated = index_element_to_be_updated -1 - - list_inp.insert(index_element_to_be_updated+1, [innner_index_pr_pos]) - if len(pos_list)>0: - list_inp.insert(index_element_to_be_updated+2, pos_list) - - len_all_elements = [len(i) for i in list_inp] - list_len_bigger_1 = np.where(np.array(len_all_elements)>1) - list_len_bigger_1 = list_len_bigger_1[0] - - if len(list_len_bigger_1)>0: - early_list_bigger_than_one = list_len_bigger_1[0] - else: - early_list_bigger_than_one = -20 - return list_inp, early_list_bigger_than_one - def do_order_of_regions_with_model_optimized_algorithm(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): - y_len = text_regions_p.shape[0] - x_len = text_regions_p.shape[1] - - img_poly = np.zeros((y_len,x_len), dtype='uint8') - - unique_pix = np.unique(text_regions_p) - - - img_poly[text_regions_p[:,:]==1] = 1 - img_poly[text_regions_p[:,:]==2] = 2 - img_poly[text_regions_p[:,:]==3] = 4 - img_poly[text_regions_p[:,:]==6] = 5 - if not self.dir_in: self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) - height1 =672#448 - width1 = 448#224 - - height2 =672#448 - width2= 448#224 - - height3 =672#448 - width3 = 448#224 - - img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') - - if contours_only_text_parent_h: - _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) - - for j in range(len(cy_main)): - img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - - co_text_all = contours_only_text_parent + contours_only_text_parent_h - else: - co_text_all = contours_only_text_parent - - - labels_con = np.zeros((y_len,x_len,len(co_text_all)),dtype='uint8') - for i in range(len(co_text_all)): - img_label = np.zeros((y_len,x_len,3),dtype='uint8') - img_label=cv2.fillPoly(img_label, pts =[co_text_all[i]], color=(1,1,1)) - labels_con[:,:,i] = img_label[:,:,0] - - - img3= np.copy(img_poly) - - labels_con = resize_image(labels_con, height1, width1) - - img_header_and_sep = resize_image(img_header_and_sep, height1, width1) - - img3= resize_image (img3, height3, width3) - - img3 = img3.astype(np.uint16) - inference_bs = 3 - input_1= np.zeros( (inference_bs, height1, width1,3)) - starting_list_of_regions = [] - if len(co_text_all)<=1: - starting_list_of_regions.append( list(range(1)) ) - else: - starting_list_of_regions.append( list(range(labels_con.shape[2])) ) + input_1 = np.zeros((inference_bs, height1, width1, 3)) + ordered = [list(range(len(co_text_all)))] index_update = 0 - index_selected = starting_list_of_regions[0] #print(labels_con.shape[2],"number of regions for reading order") while index_update>=0: - ij_list = starting_list_of_regions[index_update] - i = ij_list[0] - ij_list.pop(0) - - pr_list = [] + ij_list = ordered.pop(index_update) + i = ij_list.pop(0) + + ante_list = [] post_list = [] - - batch_counter = 0 - tot_counter = 1 - - tot_iteration = len(ij_list) - full_bs_ite= tot_iteration//inference_bs - last_bs = tot_iteration % inference_bs - - jbatch_indexer =[] + tot_counter = 0 + batch = [] for j in ij_list: - img1= np.repeat(labels_con[:,:,i][:, :, np.newaxis], 3, axis=2) - img2 = np.repeat(labels_con[:,:,j][:, :, np.newaxis], 3, axis=2) - - img2[:,:,0][img3[:,:]==5] = 2 - img2[:,:,0][img_header_and_sep[:,:]==1] = 3 - - img1[:,:,0][img3[:,:]==5] = 2 - img1[:,:,0][img_header_and_sep[:,:]==1] = 3 + img1 = labels_con[:,:,i].astype(float) + img2 = labels_con[:,:,j].astype(float) + img1[img_poly==5] = 2 + img2[img_poly==5] = 2 + img1[img_header_and_sep==1] = 3 + img2[img_header_and_sep==1] = 3 - jbatch_indexer.append(j) - - input_1[batch_counter,:,:,0] = img1[:,:,0]/3. - input_1[batch_counter,:,:,2] = img2[:,:,0]/3. - input_1[batch_counter,:,:,1] = img3[:,:]/5. + input_1[len(batch), :, :, 0] = img1 / 3. + input_1[len(batch), :, :, 2] = img2 / 3. + input_1[len(batch), :, :, 1] = img_poly / 5. - batch_counter = batch_counter+1 - - if batch_counter==inference_bs or ( (tot_counter//inference_bs)==full_bs_ite and tot_counter%inference_bs==last_bs): + tot_counter += 1 + batch.append(j) + if tot_counter % inference_bs == 0 or tot_counter == len(ij_list): y_pr = self.model_reading_order.predict(input_1 , verbose=0) - - if batch_counter==inference_bs: - iteration_batches = inference_bs - else: - iteration_batches = last_bs - for jb in range(iteration_batches): + for jb, j in enumerate(batch): if y_pr[jb][0]>=0.5: - post_list.append(jbatch_indexer[jb]) + post_list.append(j) else: - pr_list.append(jbatch_indexer[jb]) - - batch_counter = 0 - jbatch_indexer = [] - - tot_counter = tot_counter+1 - - starting_list_of_regions, index_update = self.update_list_and_return_first_with_length_bigger_than_one(index_update, i, pr_list, post_list,starting_list_of_regions) + ante_list.append(j) + batch = [] + + if len(ante_list): + ordered.insert(index_update, ante_list) + index_update += 1 + ordered.insert(index_update, [i]) + if len(post_list): + ordered.insert(index_update + 1, post_list) + + index_update = -1 + for index_next, ij_list in enumerate(ordered): + if len(ij_list) > 1: + index_update = index_next + break + + ordered = [i[0] for i in ordered] + region_ids = ['region_%04d' % i for i in range(len(co_text_all))] + return ordered, region_ids - index_sort = [i[0] for i in starting_list_of_regions ] - - REGION_ID_TEMPLATE = 'region_%04d' - order_of_texts = [] - id_of_texts = [] - for order, id_text in enumerate(index_sort): - order_of_texts.append(id_text) - id_of_texts.append( REGION_ID_TEMPLATE % order ) - - - return order_of_texts, id_of_texts def return_start_and_end_of_common_text_of_textline_ocr(self,textline_image, ind_tot): width = np.shape(textline_image)[1] height = np.shape(textline_image)[0] @@ -4980,7 +4800,7 @@ class Eynollah: if self.full_layout: if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) @@ -5007,7 +4827,7 @@ class Eynollah: else: contours_only_text_parent_h = None if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model_optimized_algorithm(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) From cfc65128b1d0bbf8cc4b0e66c2ba17b4b0729f90 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 22 Dec 2024 14:56:32 +0000 Subject: [PATCH 316/412] reduce redundancy/indentation --- src/eynollah/eynollah.py | 812 +++++++++++++++++++-------------------- 1 file changed, 404 insertions(+), 408 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 651bd17..c0603fc 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4397,10 +4397,9 @@ class Eynollah: self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) continue + img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) + self.logger.info("Enhancing took %.1fs ", time.time() - t0) if self.extract_only_images: - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) - text_regions_p_1 ,erosion_hurts, polygons_lines_xml,polygons_of_images,image_page, page_coord, cont_page = self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) ocr_all_textlines = None pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, [], [], [], [], [], cont_page, [], [], ocr_all_textlines) @@ -4413,9 +4412,8 @@ class Eynollah: continue else: return pcgts + if self.skip_layout_and_reading_order: - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, skip_layout_and_reading_order=self.skip_layout_and_reading_order) @@ -4454,463 +4452,461 @@ class Eynollah: continue else: return pcgts - if not self.extract_only_images and not self.skip_layout_and_reading_order: - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) - self.logger.info("Enhancing took %.1fs ", time.time() - t0) - #print("text region early -1 in %.1fs", time.time() - t0) - t1 = time.time() - if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) - #print("text region early -2 in %.1fs", time.time() - t0) - if num_col_classifier == 1 or num_col_classifier ==2: - if num_col_classifier == 1: - img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + #print("text region early -1 in %.1fs", time.time() - t0) + t1 = time.time() + if self.light_version: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + #print("text region early -2 in %.1fs", time.time() - t0) - elif num_col_classifier == 2: - img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) - else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - #print("text region early -2,5 in %.1fs", time.time() - t0) - #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) - #self.logger.info("run graphics %.1fs ", time.time() - t1t) - #print("text region early -3 in %.1fs", time.time() - t0) - textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) - #print("text region early -4 in %.1fs", time.time() - t0) - else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) - self.logger.info("Textregion detection took %.1fs ", time.time() - t1) - - t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ - self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) - self.logger.info("Graphics detection took %.1fs ", time.time() - t1) - #self.logger.info('cont_page %s', cont_page) - #plt.imshow(table_prediction) - #plt.show() - - if not num_col: - self.logger.info("No columns detected, outputting an empty PAGE-XML") - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - - #print("text region early in %.1fs", time.time() - t0) - t1 = time.time() - if not self.light_version: - textline_mask_tot_ea = self.run_textline(image_page) - self.logger.info("textline detection took %.1fs", time.time() - t1) - t1 = time.time() - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) - self.logger.info("deskewing took %.1fs", time.time() - t1) - elif num_col_classifier in (1,2): - org_h_l_m = textline_mask_tot_ea.shape[0] - org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: - img_w_new = 2000 + img_w_new = 1000 img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) elif num_col_classifier == 2: - img_w_new = 2400 + img_w_new = 1300 img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - image_page = resize_image(image_page,img_h_new, img_w_new ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - mask_images = resize_image(mask_images,img_h_new, img_w_new ) - mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) - text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) - table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) + textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) - - if self.light_version and num_col_classifier in (1,2): - image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) - text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) - textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) - text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) - table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) - image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) - - self.logger.info("detection of marginals took %.1fs", time.time() - t1) - #print("text region early 2 marginal in %.1fs", time.time() - t0) - ## birdan sora chock chakir - t1 = time.time() - if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ - self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) else: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ - self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) - ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) - if self.light_version: - drop_label_in_full_layout = 4 - textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + #print("text region early -2,5 in %.1fs", time.time() - t0) + #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + #self.logger.info("run graphics %.1fs ", time.time() - t1t) + #print("text region early -3 in %.1fs", time.time() - t0) + textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) + #print("text region early -4 in %.1fs", time.time() - t0) + else: + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + self.logger.info("Textregion detection took %.1fs ", time.time() - t1) + + t1 = time.time() + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) + self.logger.info("Graphics detection took %.1fs ", time.time() - t1) + #self.logger.info('cont_page %s', cont_page) + #plt.imshow(table_prediction) + #plt.show() + + if not num_col: + self.logger.info("No columns detected, outputting an empty PAGE-XML") + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t1) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts + + #print("text region early in %.1fs", time.time() - t0) + t1 = time.time() + if not self.light_version: + textline_mask_tot_ea = self.run_textline(image_page) + self.logger.info("textline detection took %.1fs", time.time() - t1) + t1 = time.time() + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + self.logger.info("deskewing took %.1fs", time.time() - t1) + elif num_col_classifier in (1,2): + org_h_l_m = textline_mask_tot_ea.shape[0] + org_w_l_m = textline_mask_tot_ea.shape[1] + if num_col_classifier == 1: + img_w_new = 2000 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + elif num_col_classifier == 2: + img_w_new = 2400 + img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + + image_page = resize_image(image_page,img_h_new, img_w_new ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) + mask_images = resize_image(mask_images,img_h_new, img_w_new ) + mask_lines = resize_image(mask_lines,img_h_new, img_w_new ) + text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) + table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) + + textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + + if self.light_version and num_col_classifier in (1,2): + image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) + textline_mask_tot_ea = resize_image(textline_mask_tot_ea,org_h_l_m, org_w_l_m ) + text_regions_p = resize_image(text_regions_p,org_h_l_m, org_w_l_m ) + textline_mask_tot = resize_image(textline_mask_tot,org_h_l_m, org_w_l_m ) + text_regions_p_1 = resize_image(text_regions_p_1,org_h_l_m, org_w_l_m ) + table_prediction = resize_image(table_prediction,org_h_l_m, org_w_l_m ) + image_page_rotated = resize_image(image_page_rotated,org_h_l_m, org_w_l_m ) + + self.logger.info("detection of marginals took %.1fs", time.time() - t1) + #print("text region early 2 marginal in %.1fs", time.time() - t0) + ## birdan sora chock chakir + t1 = time.time() + if not self.full_layout: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ + self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + else: + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ + self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) + ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) + if self.light_version: + drop_label_in_full_layout = 4 + textline_mask_tot_ea_org[img_revised_tab==drop_label_in_full_layout] = 0 - text_only = ((img_revised_tab[:, :] == 1)) * 1 + text_only = ((img_revised_tab[:, :] == 1)) * 1 + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + + #print("text region early 2 in %.1fs", time.time() - t0) + ###min_con_area = 0.000005 + contours_only_text, hir_on_text = return_contours_of_image(text_only) + contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + + if len(contours_only_text_parent) > 0: + areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) + areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) + #self.logger.info('areas_cnt_text %s', areas_cnt_text) + contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] + index_con_parents = np.argsort(areas_cnt_text_parent) + + contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + + ##try: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##except: + ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + + cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) + cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - text_only_d = ((text_regions_p_1_n[:, :] == 1)) * 1 + contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) + contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - #print("text region early 2 in %.1fs", time.time() - t0) - ###min_con_area = 0.000005 - contours_only_text, hir_on_text = return_contours_of_image(text_only) - contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) + areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) + areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - if len(contours_only_text_parent) > 0: - areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) - areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) - #self.logger.info('areas_cnt_text %s', areas_cnt_text) - contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] - areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] - index_con_parents = np.argsort(areas_cnt_text_parent) + if len(areas_cnt_text_d)>0: + contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] + index_con_parents_d = np.argsort(areas_cnt_text_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + #try: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #except: + #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) - ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) - ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) - ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) + cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) + try: + if len(cx_bigest_d) >= 5: + cx_bigest_d_last5 = cx_bigest_d[-5:] + cy_biggest_d_last5 = cy_biggest_d[-5:] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) + else: + cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] + cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) - cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) - cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) + cx_bigest_d_big[0] = cx_bigest_d[ind_largest] + cy_biggest_d_big[0] = cy_biggest_d[ind_largest] + except Exception as why: + self.logger.error(why) - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) - contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) - - areas_cnt_text_d = np.array([cv2.contourArea(c) for c in contours_only_text_parent_d]) - areas_cnt_text_d = areas_cnt_text_d / float(text_only_d.shape[0] * text_only_d.shape[1]) - - if len(areas_cnt_text_d)>0: - contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] - index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) - #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) - #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - - #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) - - cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) - cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) - try: - if len(cx_bigest_d) >= 5: - cx_bigest_d_last5 = cx_bigest_d[-5:] - cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) - else: - cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] - cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] - ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) - - cx_bigest_d_big[0] = cx_bigest_d[ind_largest] - cy_biggest_d_big[0] = cy_biggest_d[ind_largest] - except Exception as why: - self.logger.error(why) - - (h, w) = text_only.shape[:2] - center = (w // 2.0, h // 2.0) - M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) - M_22 = np.array(M)[:2, :2] - p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) - x_diff = p_big[0] - cx_bigest_d_big - y_diff = p_big[1] - cy_biggest_d_big - - contours_only_text_parent_d_ordered = [] - for i in range(len(contours_only_text_parent)): - p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) - p[0] = p[0] - x_diff[0] - p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] - contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) - # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) - # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) - # plt.imshow(img2[:,:,0]) - # plt.show() - else: - contours_only_text_parent_d_ordered = [] - contours_only_text_parent_d = [] - contours_only_text_parent = [] + (h, w) = text_only.shape[:2] + center = (w // 2.0, h // 2.0) + M = cv2.getRotationMatrix2D(center, slope_deskew, 1.0) + M_22 = np.array(M)[:2, :2] + p_big = np.dot(M_22, [cx_bigest_big, cy_biggest_big]) + x_diff = p_big[0] - cx_bigest_d_big + y_diff = p_big[1] - cy_biggest_d_big + contours_only_text_parent_d_ordered = [] + for i in range(len(contours_only_text_parent)): + p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) + p[0] = p[0] - x_diff[0] + p[1] = p[1] - y_diff[0] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) + # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) + # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) + # plt.imshow(img2[:,:,0]) + # plt.show() else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] contours_only_text_parent = [] - if not len(contours_only_text_parent): - # stop early - empty_marginals = [[]] * len(polygons_of_marginals) - if self.full_layout: - pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) - else: - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) - self.logger.info("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts - - #print("text region early 3 in %.1fs", time.time() - t0) - if self.light_version: - contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) - #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) - #txt_con_org = self.dilate_textregions_contours(txt_con_org) - #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) - #print("text region early 4 in %.1fs", time.time() - t0) - boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) - boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) - #print("text region early 5 in %.1fs", time.time() - t0) - ## birdan sora chock chakir - if not self.curved_line: - if self.light_version: - if self.textline_light: - #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + contours_only_text_parent_d_ordered = [] + contours_only_text_parent_d = [] + contours_only_text_parent = [] - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light2(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + if not len(contours_only_text_parent): + # stop early + empty_marginals = [[]] * len(polygons_of_marginals) + if self.full_layout: + pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) + else: + pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) + self.logger.info("Job done in %.1fs", time.time() - t0) + if self.dir_in: + self.writer.write_pagexml(pcgts) + continue + else: + return pcgts - #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ - # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) + #print("text region early 3 in %.1fs", time.time() - t0) + if self.light_version: + contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + #print("text region early 3.5 in %.1fs", time.time() - t0) + txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) + #txt_con_org = self.dilate_textregions_contours(txt_con_org) + #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) + else: + txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + #print("text region early 4 in %.1fs", time.time() - t0) + boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) + boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) + #print("text region early 5 in %.1fs", time.time() - t0) + ## birdan sora chock chakir + if not self.curved_line: + if self.light_version: + if self.textline_light: + #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ - # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) - #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) - #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ + self.get_slopes_and_deskew_new_light2(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) - contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, index_by_text_par_con = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered) + #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ + # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - else: - textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ + # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) + #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) + all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) + + contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, index_by_text_par_con = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered) - #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) + self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: - scale_param = 1 - textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) + textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + else: + scale_param = 1 + textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ + self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ + self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) - #print("text region early 6 in %.1fs", time.time() - t0) - if self.full_layout: - if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - else: - #takes long timee - contours_only_text_parent_d_ordered = None - if self.light_version: - fun = check_any_text_region_in_model_one_is_main_or_header_light - else: - fun = check_any_text_region_in_model_one_is_main_or_header - text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ - all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ - contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ - fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) - - if self.plotter: - self.plotter.save_plot_of_layout(text_regions_p, image_page) - self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - - pixel_img = 4 - polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, - all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, - kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) - pixel_lines = 6 - - if not self.reading_order_machine_based: - if not self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) - elif self.headers_off: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - - if num_col_classifier >= 3: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - regions_without_separators = regions_without_separators.astype(np.uint8) - regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - - else: - regions_without_separators_d = regions_without_separators_d.astype(np.uint8) - regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) - - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) - else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + #print("text region early 6 in %.1fs", time.time() - t0) + if self.full_layout: + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + else: + #takes long timee + contours_only_text_parent_d_ordered = None + if self.light_version: + fun = check_any_text_region_in_model_one_is_main_or_header_light + else: + fun = check_any_text_region_in_model_one_is_main_or_header + text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ + all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ + contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ + fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) if self.plotter: - self.plotter.write_images_into_directory(polygons_of_images, image_page) - t_order = time.time() + self.plotter.save_plot_of_layout(text_regions_p, image_page) + self.plotter.save_plot_of_layout_all(text_regions_p, image_page) - if self.full_layout: + pixel_img = 4 + polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, + all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, + kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) + pixel_lines = 6 - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) - else: + if not self.reading_order_machine_based: + if not self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + elif self.headers_off: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + else: + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) - if self.ocr: - ocr_all_textlines = [] - else: - ocr_all_textlines = None - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, - all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, - cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - #print("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + if num_col_classifier >= 3: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + regions_without_separators = regions_without_separators.astype(np.uint8) + regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) + else: + regions_without_separators_d = regions_without_separators_d.astype(np.uint8) + regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) + + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + else: + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + + if self.plotter: + self.plotter.write_images_into_directory(polygons_of_images, image_page) + t_order = time.time() + + if self.full_layout: + + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: - contours_only_text_parent_h = None - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) - else: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) - #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) - #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - - - if self.ocr: - - device = cuda.get_current_device() - device.reset() - gc.collect() - model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") - torch.cuda.empty_cache() - model_ocr.to(device) - - ind_tot = 0 - #cv2.imwrite('./img_out.png', image_page) - - ocr_all_textlines = [] - for indexing, ind_poly_first in enumerate(all_found_textline_polygons): - ocr_textline_in_textregion = [] - for indexing2, ind_poly in enumerate(ind_poly_first): - if not (self.textline_light or self.curved_line): - ind_poly = copy.deepcopy(ind_poly) - box_ind = all_box_coord[indexing] - #print(ind_poly,np.shape(ind_poly), 'ind_poly') - #print(box_ind) - ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) - #print(ind_poly_copy) - ind_poly[ind_poly<0] = 0 - x, y, w, h = cv2.boundingRect(ind_poly) - #print(ind_poly_copy, np.shape(ind_poly_copy)) - #print(x, y, w, h, h/float(w),'ratio') - h2w_ratio = h/float(w) - mask_poly = np.zeros(image_page.shape) - if not self.light_version: - img_poly_on_img = np.copy(image_page) - else: - img_poly_on_img = np.copy(img_bin_light) - - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) - - if self.textline_light: - mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) - img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 - img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 - - img_croped = img_poly_on_img[y:y+h, x:x+w, :] - #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) - text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) - - ocr_textline_in_textregion.append(text_ocr) - - - ind_tot = ind_tot +1 - ocr_all_textlines.append(ocr_textline_in_textregion) - - else: - ocr_all_textlines = None - #print(ocr_all_textlines) - self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, - cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - #print("Job done in %.1fs" % (time.time() - t0)) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + if self.ocr: + ocr_all_textlines = [] + else: + ocr_all_textlines = None + pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, + cont_page, polygons_lines_xml, ocr_all_textlines) + self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs", time.time() - t0) if self.dir_in: self.writer.write_pagexml(pcgts) - self.logger.info("Job done in %.1fs", time.time() - t0) + continue + else: + return pcgts + + else: + contours_only_text_parent_h = None + if self.reading_order_machine_based: + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + else: + if np.abs(slope_deskew) < SLOPE_THRESHOLD: + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + else: + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + #try: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #except: + #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) + + + if self.ocr: + + device = cuda.get_current_device() + device.reset() + gc.collect() + model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") + torch.cuda.empty_cache() + model_ocr.to(device) + + ind_tot = 0 + #cv2.imwrite('./img_out.png', image_page) + + ocr_all_textlines = [] + for indexing, ind_poly_first in enumerate(all_found_textline_polygons): + ocr_textline_in_textregion = [] + for indexing2, ind_poly in enumerate(ind_poly_first): + if not (self.textline_light or self.curved_line): + ind_poly = copy.deepcopy(ind_poly) + box_ind = all_box_coord[indexing] + #print(ind_poly,np.shape(ind_poly), 'ind_poly') + #print(box_ind) + ind_poly = self.return_textline_contour_with_added_box_coordinate(ind_poly, box_ind) + #print(ind_poly_copy) + ind_poly[ind_poly<0] = 0 + x, y, w, h = cv2.boundingRect(ind_poly) + #print(ind_poly_copy, np.shape(ind_poly_copy)) + #print(x, y, w, h, h/float(w),'ratio') + h2w_ratio = h/float(w) + mask_poly = np.zeros(image_page.shape) + if not self.light_version: + img_poly_on_img = np.copy(image_page) + else: + img_poly_on_img = np.copy(img_bin_light) + + mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) + + if self.textline_light: + mask_poly = cv2.dilate(mask_poly, KERNEL, iterations=1) + img_poly_on_img[:,:,0][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,1][mask_poly[:,:,0] ==0] = 255 + img_poly_on_img[:,:,2][mask_poly[:,:,0] ==0] = 255 + + img_croped = img_poly_on_img[y:y+h, x:x+w, :] + #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) + text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) + + ocr_textline_in_textregion.append(text_ocr) + + + ind_tot = ind_tot +1 + ocr_all_textlines.append(ocr_textline_in_textregion) + + else: + ocr_all_textlines = None + #print(ocr_all_textlines) + self.logger.info("detection of reading order took %.1fs", time.time() - t_order) + pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) #print("Job done in %.1fs" % (time.time() - t0)) + self.logger.info("Job done in %.1fs", time.time() - t0) + if not self.dir_in: + return pcgts + #print("text region early 7 in %.1fs", time.time() - t0) + + if self.dir_in: + self.writer.write_pagexml(pcgts) + self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs" % (time.time() - t0)) if self.dir_in: self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) From 335aa273a1c71587c42a55858730cebb5b82c55e Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 23 Dec 2024 03:13:21 +0000 Subject: [PATCH 317/412] simplify, wrap extremely long lines --- src/eynollah/eynollah.py | 1965 +++++++++++++------------- src/eynollah/utils/__init__.py | 1349 ++++++++---------- src/eynollah/utils/contour.py | 156 +- src/eynollah/utils/separate_lines.py | 306 ++-- 4 files changed, 1792 insertions(+), 1984 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index c0603fc..25d5ec4 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -6,6 +6,7 @@ document layout analysis (segmentation) with output in PAGE-XML """ +import tracemalloc import math import os import sys @@ -266,20 +267,45 @@ class Eynollah: self.model_dir_of_col_classifier = dir_models + "/eynollah-column-classifier_20210425" self.model_region_dir_p = dir_models + "/eynollah-main-regions-aug-scaling_20210425" self.model_region_dir_p2 = dir_models + "/eynollah-main-regions-aug-rotation_20210425" - self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_1__4_3_091124"#"/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/eynollah-full-regions-1column_20210425" + #"/modelens_full_lay_1_3_031124" + #"/modelens_full_lay_13__3_19_241024" + #"/model_full_lay_13_241024" + #"/modelens_full_lay_13_17_231024" + #"/modelens_full_lay_1_2_221024" + #"/eynollah-full-regions-1column_20210425" + self.model_region_dir_fully_np = dir_models + "/modelens_full_lay_1__4_3_091124" #self.model_region_dir_fully = dir_models + "/eynollah-full-regions-3+column_20210425" self.model_page_dir = dir_models + "/eynollah-page-extraction_20210425" self.model_region_dir_p_ens = dir_models + "/eynollah-main-regions-ensembled_20210425" self.model_region_dir_p_ens_light = dir_models + "/eynollah-main-regions_20220314" self.model_region_dir_p_ens_light_only_images_extraction = dir_models + "/eynollah-main-regions_20231127_672_org_ens_11_13_16_17_18" self.model_reading_order_dir = dir_models + "/model_ens_reading_order_machine_based" - self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024"#"/modelens_12sp_elay_0_3_4__3_6_n"#"/modelens_earlylayout_12spaltige_2_3_5_6_7_8"#"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18"#"/modelens_1_2_4_5_early_lay_1_2_spaltige"#"/model_3_eraly_layout_no_patches_1_2_spaltige" + #"/modelens_12sp_elay_0_3_4__3_6_n" + #"/modelens_earlylayout_12spaltige_2_3_5_6_7_8" + #"/modelens_early12_sp_2_3_5_6_7_8_9_10_12_14_15_16_18" + #"/modelens_1_2_4_5_early_lay_1_2_spaltige" + #"/model_3_eraly_layout_no_patches_1_2_spaltige" + self.model_region_dir_p_1_2_sp_np = dir_models + "/modelens_e_l_all_sp_0_1_2_3_4_171024" ##self.model_region_dir_fully_new = dir_models + "/model_2_full_layout_new_trans" - self.model_region_dir_fully = dir_models + "/modelens_full_lay_1__4_3_091124"#"/modelens_full_lay_1_3_031124"#"/modelens_full_lay_13__3_19_241024"#"/model_full_lay_13_241024"#"/modelens_full_lay_13_17_231024"#"/modelens_full_lay_1_2_221024"#"/modelens_full_layout_24_till_28"#"/model_2_full_layout_new_trans" + #"/modelens_full_lay_1_3_031124" + #"/modelens_full_lay_13__3_19_241024" + #"/model_full_lay_13_241024" + #"/modelens_full_lay_13_17_231024" + #"/modelens_full_lay_1_2_221024" + #"/modelens_full_layout_24_till_28" + #"/model_2_full_layout_new_trans" + self.model_region_dir_fully = dir_models + "/modelens_full_lay_1__4_3_091124" if self.textline_light: - self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/modelens_textline_1_4_16092024"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_1_3_4_20240915"#"/model_textline_ens_3_4_5_6_artificial"#"/modelens_textline_9_12_13_14_15"#"/eynollah-textline_light_20210425"# + #"/modelens_textline_1_4_16092024" + #"/model_textline_ens_3_4_5_6_artificial" + #"/modelens_textline_1_3_4_20240915" + #"/model_textline_ens_3_4_5_6_artificial" + #"/modelens_textline_9_12_13_14_15" + #"/eynollah-textline_light_20210425" + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024" else: - self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024"#"/eynollah-textline_20210425" + #"/eynollah-textline_20210425" + self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024" if self.ocr: self.model_ocr_dir = dir_models + "/trocr_model_ens_of_3_checkpoints_201124" @@ -320,13 +346,13 @@ class Eynollah: if self.ocr: self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten")#("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") + #("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") + self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten") if self.tables: self.model_table = self.our_load_model(self.model_table_dir) self.ls_imgs = os.listdir(self.dir_in) - - + def _cache_images(self, image_filename=None, image_pil=None): ret = {} t_c0 = time.time() @@ -346,6 +372,7 @@ class Eynollah: for prefix in ('', '_grayscale'): ret[f'img{prefix}_uint8'] = ret[f'img{prefix}'].astype(np.uint8) return ret + def reset_file_name_dir(self, image_filename): t_c = time.time() self._imgs = self._cache_images(image_filename=image_filename) @@ -378,31 +405,27 @@ class Eynollah: def isNaN(self, num): return num != num - def predict_enhancement(self, img): self.logger.debug("enter predict_enhancement") if not self.dir_in: self.model_enhancement, _ = self.start_new_session_and_model(self.model_dir_of_enhancement) - img_height_model = self.model_enhancement.layers[len(self.model_enhancement.layers) - 1].output_shape[1] - img_width_model = self.model_enhancement.layers[len(self.model_enhancement.layers) - 1].output_shape[2] + img_height_model = self.model_enhancement.layers[-1].output_shape[1] + img_width_model = self.model_enhancement.layers[-1].output_shape[2] if img.shape[0] < img_height_model: img = cv2.resize(img, (img.shape[1], img_width_model), interpolation=cv2.INTER_NEAREST) - if img.shape[1] < img_width_model: img = cv2.resize(img, (img_height_model, img.shape[0]), interpolation=cv2.INTER_NEAREST) margin = int(0 * img_width_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin - img = img / float(255.0) - + img = img / 255. img_h = img.shape[0] img_w = img.shape[1] prediction_true = np.zeros((img_h, img_w, 3)) nxf = img_w / float(width_mid) nyf = img_h / float(height_mid) - nxf = int(nxf) + 1 if nxf > int(nxf) else int(nxf) nyf = int(nyf) + 1 if nyf > int(nyf) else int(nyf) @@ -430,37 +453,53 @@ class Eynollah: img_patch = img[np.newaxis, index_y_d:index_y_u, index_x_d:index_x_u, :] label_p_pred = self.model_enhancement.predict(img_patch, verbose=0) - - seg = label_p_pred[0, :, :, :] - seg = seg * 255 + seg = label_p_pred[0, :, :, :] * 255 if i == 0 and j == 0: - seg = seg[0 : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg + prediction_true[index_y_d + 0:index_y_u - margin, + index_x_d + 0:index_x_u - margin] = \ + seg[0:-margin or None, + 0:-margin or None] elif i == nxf - 1 and j == nyf - 1: - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - 0] - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - 0, :] = seg + prediction_true[index_y_d + margin:index_y_u - 0, + index_x_d + margin:index_x_u - 0] = \ + seg[margin:, + margin:] elif i == 0 and j == nyf - 1: - seg = seg[margin : seg.shape[0] - 0, 0 : seg.shape[1] - margin] - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + 0 : index_x_u - margin, :] = seg + prediction_true[index_y_d + margin:index_y_u - 0, + index_x_d + 0:index_x_u - margin] = \ + seg[margin:, + 0:-margin or None] elif i == nxf - 1 and j == 0: - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - 0] - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg + prediction_true[index_y_d + 0:index_y_u - margin, + index_x_d + margin:index_x_u - 0] = \ + seg[0:-margin or None, + margin:] elif i == 0 and j != 0 and j != nyf - 1: - seg = seg[margin : seg.shape[0] - margin, 0 : seg.shape[1] - margin] - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + 0 : index_x_u - margin, :] = seg + prediction_true[index_y_d + margin:index_y_u - margin, + index_x_d + 0:index_x_u - margin] = \ + seg[margin:-margin or None, + 0:-margin or None] elif i == nxf - 1 and j != 0 and j != nyf - 1: - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - 0] - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - 0, :] = seg + prediction_true[index_y_d + margin:index_y_u - margin, + index_x_d + margin:index_x_u - 0] = \ + seg[margin:-margin or None, + margin:] elif i != 0 and i != nxf - 1 and j == 0: - seg = seg[0 : seg.shape[0] - margin, margin : seg.shape[1] - margin] - prediction_true[index_y_d + 0 : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg + prediction_true[index_y_d + 0:index_y_u - margin, + index_x_d + margin:index_x_u - margin] = \ + seg[0:-margin or None, + margin:-margin or None] elif i != 0 and i != nxf - 1 and j == nyf - 1: - seg = seg[margin : seg.shape[0] - 0, margin : seg.shape[1] - margin] - prediction_true[index_y_d + margin : index_y_u - 0, index_x_d + margin : index_x_u - margin, :] = seg + prediction_true[index_y_d + margin:index_y_u - 0, + index_x_d + margin:index_x_u - margin] = \ + seg[margin:, + margin:-margin or None] else: - seg = seg[margin : seg.shape[0] - margin, margin : seg.shape[1] - margin] - prediction_true[index_y_d + margin : index_y_u - margin, index_x_d + margin : index_x_u - margin, :] = seg + prediction_true[index_y_d + margin:index_y_u - margin, + index_x_d + margin:index_x_u - margin] = \ + seg[margin:-margin or None, + margin:-margin or None] prediction_true = prediction_true.astype(int) return prediction_true @@ -469,55 +508,39 @@ class Eynollah: self.logger.debug("enter calculate_width_height_by_columns") if num_col == 1 and width_early < 1100: img_w_new = 2000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 2000) elif num_col == 1 and width_early >= 2500: img_w_new = 2000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 2000) elif num_col == 1 and width_early >= 1100 and width_early < 2500: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) elif num_col == 2 and width_early < 2000: img_w_new = 2400 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 2400) elif num_col == 2 and width_early >= 3500: img_w_new = 2400 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 2400) elif num_col == 2 and width_early >= 2000 and width_early < 3500: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) elif num_col == 3 and width_early < 2000: img_w_new = 3000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 3000) elif num_col == 3 and width_early >= 4000: img_w_new = 3000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 3000) elif num_col == 3 and width_early >= 2000 and width_early < 4000: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) elif num_col == 4 and width_early < 2500: img_w_new = 4000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 4000) elif num_col == 4 and width_early >= 5000: img_w_new = 4000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 4000) elif num_col == 4 and width_early >= 2500 and width_early < 5000: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) elif num_col == 5 and width_early < 3700: img_w_new = 5000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 5000) elif num_col == 5 and width_early >= 7000: img_w_new = 5000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 5000) elif num_col == 5 and width_early >= 3700 and width_early < 7000: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) elif num_col == 6 and width_early < 4500: img_w_new = 6500 # 5400 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 6500) else: img_w_new = width_early - img_h_new = int(img.shape[0] / float(img.shape[1]) * width_early) + img_h_new = img_w_new * img.shape[0] // img.shape[1] if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) @@ -536,10 +559,9 @@ class Eynollah: self.logger.debug("enter calculate_width_height_by_columns") if num_col == 1: img_w_new = 1000 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 1000) else: img_w_new = 1300 - img_h_new = int(img.shape[0] / float(img.shape[1]) * 1300) + img_h_new = img_w_new * img.shape[0] // img.shape[1] if label_p_pred[0][int(num_col - 1)] < 0.9 and img_w_new < width_early: img_new = np.copy(img) @@ -568,7 +590,7 @@ class Eynollah: img_w_new = 2200 elif num_col == 6: img_w_new = 2500 - img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) + img_h_new = img_w_new * img.shape[0] // img.shape[1] img_new = resize_image(img, img_h_new, img_w_new) num_column_is_classified = True @@ -601,7 +623,6 @@ class Eynollah: # plt.imshow(img_1ch) # plt.show() img_1ch = img_1ch / 255.0 - img_1ch = cv2.resize(img_1ch, (448, 448), interpolation=cv2.INTER_NEAREST) img_in = np.zeros((1, img_1ch.shape[0], img_1ch.shape[1], 3)) @@ -610,11 +631,9 @@ class Eynollah: img_in[0, :, :, 2] = img_1ch[:, :] label_p_pred = self.model_classifier.predict(img_in, verbose=0) - num_col = np.argmax(label_p_pred[0]) + 1 self.logger.info("Found %s columns (%s)", num_col, label_p_pred) - img_new, _ = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) if img_new.shape[1] > img.shape[1]: @@ -623,7 +642,7 @@ class Eynollah: return img, img_new, is_image_enhanced - def resize_and_enhance_image_with_column_classifier(self,light_version): + def resize_and_enhance_image_with_column_classifier(self, light_version): self.logger.debug("enter resize_and_enhance_image_with_column_classifier") dpi = self.dpi self.logger.info("Detected %s DPI", dpi) @@ -633,16 +652,10 @@ class Eynollah: self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img, self.model_bin, n_batch_inference=5) - - prediction_bin=prediction_bin[:,:,0] - prediction_bin = (prediction_bin[:,:]==0)*1 - prediction_bin = prediction_bin*255 - - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - prediction_bin = prediction_bin.astype(np.uint8) + prediction_bin = 255 * (prediction_bin[:,:,0]==0) + prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2).astype(np.uint8) img= np.copy(prediction_bin) - img_bin = np.copy(prediction_bin) + img_bin = prediction_bin else: img = self.imread() img_bin = None @@ -663,8 +676,7 @@ class Eynollah: elif self.num_col_lower and not self.num_col_upper: num_col = self.num_col_lower label_p_pred = [np.ones(6)] - - elif (not self.num_col_upper and not self.num_col_lower): + elif not self.num_col_upper and not self.num_col_lower: if self.input_binary: img_in = np.copy(img) img_in = img_in / 255.0 @@ -682,7 +694,6 @@ class Eynollah: img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] - label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 elif (self.num_col_upper and self.num_col_lower) and (self.num_col_upper!=self.num_col_lower): @@ -703,7 +714,6 @@ class Eynollah: img_in[0, :, :, 1] = img_1ch[:, :] img_in[0, :, :, 2] = img_1ch[:, :] - label_p_pred = self.model_classifier.predict(img_in, verbose=0) num_col = np.argmax(label_p_pred[0]) + 1 @@ -713,20 +723,19 @@ class Eynollah: if num_col < self.num_col_lower: num_col = self.num_col_lower label_p_pred = [np.ones(6)] - else: num_col = self.num_col_upper label_p_pred = [np.ones(6)] - self.logger.info("Found %d columns (%s)", num_col, np.around(label_p_pred, decimals=5)) - if not self.extract_only_images: if dpi < DPI_THRESHOLD: if light_version and num_col in (1,2): - img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2(img, num_col, width_early, label_p_pred) + img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2( + img, num_col, width_early, label_p_pred) else: - img_new, num_column_is_classified = self.calculate_width_height_by_columns(img, num_col, width_early, label_p_pred) + img_new, num_column_is_classified = self.calculate_width_height_by_columns( + img, num_col, width_early, label_p_pred) if light_version: image_res = np.copy(img_new) else: @@ -734,7 +743,8 @@ class Eynollah: is_image_enhanced = True else: if light_version and num_col in (1,2): - img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2(img, num_col, width_early, label_p_pred) + img_new, num_column_is_classified = self.calculate_width_height_by_columns_1_2( + img, num_col, width_early, label_p_pred) image_res = np.copy(img_new) is_image_enhanced = True else: @@ -809,7 +819,6 @@ class Eynollah: return model, session - def start_new_session_and_model(self, model_dir): self.logger.debug("enter start_new_session_and_model (model_dir=%s)", model_dir) #gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) @@ -830,17 +839,20 @@ class Eynollah: else: try: model = load_model(model_dir, compile=False) - self.models[model_dir] = model except: - model = load_model(model_dir , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) - self.models[model_dir] = model - + model = load_model(model_dir , compile=False, custom_objects={ + "PatchEncoder": PatchEncoder, "Patches": Patches}) + self.models[model_dir] = model return model, None - def do_prediction(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): - self.logger.debug("enter do_prediction") + def do_prediction( + self, patches, img, model, + n_batch_inference=1, marginal_of_patch_percent=0.1, + thresholding_for_some_classes_in_light_version=False, + thresholding_for_artificial_class_in_light_version=False): + self.logger.debug("enter do_prediction") img_height_model = model.layers[-1].output_shape[1] img_width_model = model.layers[-1].output_shape[2] @@ -851,7 +863,6 @@ class Eynollah: img = resize_image(img, img_height_model, img_width_model) label_p_pred = model.predict(img[np.newaxis], verbose=0) - seg = np.argmax(label_p_pred, axis=3)[0] if thresholding_for_artificial_class_in_light_version: @@ -862,13 +873,11 @@ class Eynollah: seg[seg_art==1]=2 seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) - prediction_true = resize_image(seg_color, img_h_page, img_w_page) - prediction_true = prediction_true.astype(np.uint8) + prediction_true = resize_image(seg_color, img_h_page, img_w_page).astype(np.uint8) return prediction_true if img.shape[0] < img_height_model: img = resize_image(img, img_height_model, img.shape[1]) - if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) @@ -876,7 +885,7 @@ class Eynollah: margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin - img = img / float(255.0) + img = img / 255. #img = img.astype(np.float16) img_h = img.shape[0] img_w = img.shape[1] @@ -895,7 +904,6 @@ class Eynollah: list_y_d = [] batch_indexer = 0 - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) for i in range(nxf): for j in range(nyf): @@ -925,17 +933,14 @@ class Eynollah: list_y_d.append(index_y_d) list_y_u.append(index_y_u) - img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - - batch_indexer = batch_indexer + 1 + batch_indexer += 1 if (batch_indexer == n_batch_inference or # last batch i == nxf - 1 and j == nyf - 1): self.logger.debug("predicting patches on %s", str(img_patch.shape)) - label_p_pred = model.predict(img_patch,verbose=0) - + label_p_pred = model.predict(img_patch, verbose=0) seg = np.argmax(label_p_pred, axis=3) if thresholding_for_some_classes_in_light_version: @@ -964,8 +969,7 @@ class Eynollah: indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + seg_in = seg[indexer_inside_batch] index_y_u_in = list_y_u[indexer_inside_batch] index_y_d_in = list_y_d[indexer_inside_batch] @@ -974,34 +978,60 @@ class Eynollah: index_x_d_in = list_x_d[indexer_inside_batch] if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[0:-margin or None, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[margin:, + margin:, + np.newaxis] elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[margin:, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[0:-margin or None, + margin:, + np.newaxis] elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[margin:-margin or None, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[margin:-margin or None, + margin:, + np.newaxis] elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[0:-margin or None, + margin:-margin or None, + np.newaxis] elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[margin:, + margin:-margin or None, + np.newaxis] else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[margin:-margin or None, + margin:-margin or None, + np.newaxis] + indexer_inside_batch += 1 list_i_s = [] @@ -1012,15 +1042,14 @@ class Eynollah: list_y_d = [] batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + img_patch[:] = 0 prediction_true = prediction_true.astype(np.uint8) #del model gc.collect() return prediction_true - def do_padding_with_scale(self,img, scale): + def do_padding_with_scale(self, img, scale): h_n = int(img.shape[0]*scale) w_n = int(img.shape[1]*scale) @@ -1031,8 +1060,8 @@ class Eynollah: h_diff = img.shape[0] - h_n w_diff = img.shape[1] - w_n - h_start = int(h_diff / 2.) - w_start = int(w_diff / 2.) + h_start = int(0.5 * h_diff) + w_start = int(0.5 * w_diff) img_res = resize_image(img, h_n, w_n) #label_res = resize_image(label, h_n, w_n) @@ -1049,9 +1078,14 @@ class Eynollah: #label_scaled_padded[h_start:h_start+h_n, w_start:w_start+w_n,:] = label_res[:,:,:] return img_scaled_padded#, label_scaled_padded - def do_prediction_new_concept_scatter_nd(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): - self.logger.debug("enter do_prediction_new_concept") + def do_prediction_new_concept_scatter_nd( + self, patches, img, model, + n_batch_inference=1, marginal_of_patch_percent=0.1, + thresholding_for_some_classes_in_light_version=False, + thresholding_for_artificial_class_in_light_version=False): + + self.logger.debug("enter do_prediction_new_concept") img_height_model = model.layers[-1].output_shape[1] img_width_model = model.layers[-1].output_shape[2] @@ -1074,16 +1108,13 @@ class Eynollah: seg_art[seg_art<0.2] =0 seg_art[seg_art>0] =1 seg[seg_art==1]=4 - seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) - prediction_true = resize_image(seg_color, img_h_page, img_w_page) - prediction_true = prediction_true.astype(np.uint8) + prediction_true = resize_image(seg_color, img_h_page, img_w_page).astype(np.uint8) return prediction_true if img.shape[0] < img_height_model: img = resize_image(img, img_height_model, img.shape[1]) - if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) @@ -1091,8 +1122,7 @@ class Eynollah: ##margin = int(marginal_of_patch_percent * img_height_model) #width_mid = img_width_model - 2 * margin #height_mid = img_height_model - 2 * margin - img = img / float(255.0) - + img = img / 255.0 img = img.astype(np.float16) img_h = img.shape[0] img_w = img.shape[1] @@ -1101,61 +1131,61 @@ class Eynollah: stride_y = img_height_model - 100 one_tensor = tf.ones_like(img) - img_patches = tf.image.extract_patches(images=[img,one_tensor], - sizes=[1, img_height_model, img_width_model, 1], - strides=[1, stride_y, stride_x, 1], - rates=[1, 1, 1, 1], - padding='SAME') - - one_patches = img_patches[1] - img_patches = img_patches[0] + img_patches, one_patches = tf.image.extract_patches( + images=[img, one_tensor], + sizes=[1, img_height_model, img_width_model, 1], + strides=[1, stride_y, stride_x, 1], + rates=[1, 1, 1, 1], + padding='SAME') img_patches = tf.squeeze(img_patches) - - img_patches_resh = tf.reshape(img_patches, shape = (img_patches.shape[0]*img_patches.shape[1], img_height_model, img_width_model, 3)) - - pred_patches = model.predict(img_patches_resh, batch_size=n_batch_inference) - one_patches = tf.squeeze(one_patches) - one_patches = tf.reshape(one_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model,img_width_model,3]) - + img_patches_resh = tf.reshape(img_patches, shape=(img_patches.shape[0] * img_patches.shape[1], + img_height_model, img_width_model, 3)) + pred_patches = model.predict(img_patches_resh, batch_size=n_batch_inference) + one_patches = tf.reshape(one_patches, shape=(img_patches.shape[0] * img_patches.shape[1], + img_height_model, img_width_model, 3)) x = tf.range(img.shape[1]) y = tf.range(img.shape[0]) x, y = tf.meshgrid(x, y) indices = tf.stack([y, x], axis=-1) - indices_patches = tf.image.extract_patches(images=tf.expand_dims(indices, axis=0), sizes=[1, img_height_model, img_width_model, 1], strides=[1, stride_y, stride_x, 1], rates=[1, 1, 1, 1], padding='SAME') + indices_patches = tf.image.extract_patches( + images=tf.expand_dims(indices, axis=0), + sizes=[1, img_height_model, img_width_model, 1], + strides=[1, stride_y, stride_x, 1], + rates=[1, 1, 1, 1], + padding='SAME') indices_patches = tf.squeeze(indices_patches) - indices_patches = tf.reshape(indices_patches, [img_patches.shape[0]*img_patches.shape[1],img_height_model, img_width_model,2]) - - margin_y = int( (img_height_model - stride_y)/2. ) - margin_x = int( (img_width_model - stride_x)/2. ) + indices_patches = tf.reshape(indices_patches, shape=(img_patches.shape[0] * img_patches.shape[1], + img_height_model, img_width_model, 2)) + margin_y = int( 0.5 * (img_height_model - stride_y) ) + margin_x = int( 0.5 * (img_width_model - stride_x) ) mask_margin = np.zeros((img_height_model, img_width_model)) - - mask_margin[margin_y:img_height_model-margin_y, margin_x:img_width_model-margin_x] = 1 + mask_margin[margin_y:img_height_model - margin_y, + margin_x:img_width_model - margin_x] = 1 indices_patches_array = indices_patches.numpy() - for i in range(indices_patches_array.shape[0]): indices_patches_array[i,:,:,0] = indices_patches_array[i,:,:,0]*mask_margin indices_patches_array[i,:,:,1] = indices_patches_array[i,:,:,1]*mask_margin - reconstructed = tf.scatter_nd(indices=indices_patches_array, updates=pred_patches, shape=(img.shape[0],img.shape[1],pred_patches.shape[-1])) - reconstructed_argmax = reconstructed.numpy() - - prediction_true = np.argmax(reconstructed_argmax, axis=2) - prediction_true = prediction_true.astype(np.uint8) + reconstructed = tf.scatter_nd( + indices=indices_patches_array, + updates=pred_patches, + shape=(img.shape[0], img.shape[1], pred_patches.shape[-1])).numpy() + prediction_true = np.argmax(reconstructed, axis=2).astype(np.uint8) gc.collect() return np.repeat(prediction_true[:, :, np.newaxis], 3, axis=2) - - - - - - def do_prediction_new_concept(self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, thresholding_for_some_classes_in_light_version=False, thresholding_for_artificial_class_in_light_version=False): - self.logger.debug("enter do_prediction_new_concept") + def do_prediction_new_concept( + self, patches, img, model, + n_batch_inference=1, marginal_of_patch_percent=0.1, + thresholding_for_some_classes_in_light_version=False, + thresholding_for_artificial_class_in_light_version=False): + + self.logger.debug("enter do_prediction_new_concept") img_height_model = model.layers[-1].output_shape[1] img_width_model = model.layers[-1].output_shape[2] @@ -1178,16 +1208,13 @@ class Eynollah: seg_art[seg_art<0.2] =0 seg_art[seg_art>0] =1 seg[seg_art==1]=4 - seg_color = np.repeat(seg[:, :, np.newaxis], 3, axis=2) - prediction_true = resize_image(seg_color, img_h_page, img_w_page) - prediction_true = prediction_true.astype(np.uint8) + prediction_true = resize_image(seg_color, img_h_page, img_w_page).astype(np.uint8) return prediction_true if img.shape[0] < img_height_model: img = resize_image(img, img_height_model, img.shape[1]) - if img.shape[1] < img_width_model: img = resize_image(img, img.shape[0], img_width_model) @@ -1195,7 +1222,7 @@ class Eynollah: margin = int(marginal_of_patch_percent * img_height_model) width_mid = img_width_model - 2 * margin height_mid = img_height_model - 2 * margin - img = img / float(255.0) + img = img / 255.0 img = img.astype(np.float16) img_h = img.shape[0] img_w = img.shape[1] @@ -1215,7 +1242,6 @@ class Eynollah: batch_indexer = 0 img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) - for i in range(nxf): for j in range(nyf): if i == 0: @@ -1237,7 +1263,6 @@ class Eynollah: index_y_u = img_h index_y_d = img_h - img_height_model - list_i_s.append(i) list_j_s.append(j) list_x_u.append(index_x_u) @@ -1245,17 +1270,14 @@ class Eynollah: list_y_d.append(index_y_d) list_y_u.append(index_y_u) - - img_patch[batch_indexer,:,:,:] = img[index_y_d:index_y_u, index_x_d:index_x_u, :] - - batch_indexer = batch_indexer + 1 + img_patch[batch_indexer] = img[index_y_d:index_y_u, index_x_d:index_x_u] + batch_indexer += 1 if (batch_indexer == n_batch_inference or # last batch i == nxf - 1 and j == nyf - 1): self.logger.debug("predicting patches on %s", str(img_patch.shape)) label_p_pred = model.predict(img_patch,verbose=0) - seg = np.argmax(label_p_pred, axis=3) if thresholding_for_some_classes_in_light_version: @@ -1279,8 +1301,7 @@ class Eynollah: indexer_inside_batch = 0 for i_batch, j_batch in zip(list_i_s, list_j_s): - seg_in = seg[indexer_inside_batch,:,:] - seg_color = np.repeat(seg_in[:, :, np.newaxis], 3, axis=2) + seg_in = seg[indexer_inside_batch] index_y_u_in = list_y_u[indexer_inside_batch] index_y_d_in = list_y_d[indexer_inside_batch] @@ -1289,35 +1310,60 @@ class Eynollah: index_x_d_in = list_x_d[indexer_inside_batch] if i_batch == 0 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[0:-margin or None, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[margin:, + margin:, + np.newaxis] elif i_batch == 0 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[margin:, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[0:-margin or None, + margin:, + np.newaxis] elif i_batch == 0 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, 0 : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + 0 : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + 0:index_x_u_in - margin] = \ + seg_in[margin:-margin or None, + 0:-margin or None, + np.newaxis] elif i_batch == nxf - 1 and j_batch != 0 and j_batch != nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - 0, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - 0, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - 0] = \ + seg_in[margin:-margin or None, + margin:, + np.newaxis] elif i_batch != 0 and i_batch != nxf - 1 and j_batch == 0: - seg_color = seg_color[0 : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + 0 : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + 0:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[0:-margin or None, + margin:-margin or None, + np.newaxis] elif i_batch != 0 and i_batch != nxf - 1 and j_batch == nyf - 1: - seg_color = seg_color[margin : seg_color.shape[0] - 0, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - 0, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color + prediction_true[index_y_d_in + margin:index_y_u_in - 0, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[margin:, + margin:-margin or None, + np.newaxis] else: - seg_color = seg_color[margin : seg_color.shape[0] - margin, margin : seg_color.shape[1] - margin, :] - prediction_true[index_y_d_in + margin : index_y_u_in - margin, index_x_d_in + margin : index_x_u_in - margin, :] = seg_color - - indexer_inside_batch = indexer_inside_batch +1 - + prediction_true[index_y_d_in + margin:index_y_u_in - margin, + index_x_d_in + margin:index_x_u_in - margin] = \ + seg_in[margin:-margin or None, + margin:-margin or None, + np.newaxis] + indexer_inside_batch += 1 list_i_s = [] list_j_s = [] @@ -1327,8 +1373,7 @@ class Eynollah: list_y_d = [] batch_indexer = 0 - - img_patch = np.zeros((n_batch_inference, img_height_model, img_width_model, 3)) + img_patch[:] = 0 prediction_true = prediction_true.astype(np.uint8) gc.collect() @@ -1338,11 +1383,10 @@ class Eynollah: self.logger.debug("enter extract_page") cont_page = [] if not self.ignore_page_extraction: - img = cv2.GaussianBlur(self.image, (5, 5), 0) - if not self.dir_in: self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) - + + img = cv2.GaussianBlur(self.image, (5, 5), 0) img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -1350,7 +1394,8 @@ class Eynollah: contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt_size = np.array([cv2.contourArea(contours[j]) + for j in range(len(contours))]) cnt = contours[np.argmax(cnt_size)] x, y, w, h = cv2.boundingRect(cnt) if x <= 30: @@ -1363,32 +1408,34 @@ class Eynollah: y = 0 if (self.image.shape[0] - (y + h)) <= 30: h = h + (self.image.shape[0] - (y + h)) - box = [x, y, w, h] else: box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, self.image) - cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - + cropped_page, page_coord = crop_image_inside_box(box, self.image) + cont_page.append(np.array([[page_coord[2], page_coord[0]], + [page_coord[3], page_coord[0]], + [page_coord[3], page_coord[1]], + [page_coord[2], page_coord[1]]])) self.logger.debug("exit extract_page") else: box = [0, 0, self.image.shape[1], self.image.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, self.image) - cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - return croped_page, page_coord, cont_page + cropped_page, page_coord = crop_image_inside_box(box, self.image) + cont_page.append(np.array([[page_coord[2], page_coord[0]], + [page_coord[3], page_coord[0]], + [page_coord[3], page_coord[1]], + [page_coord[2], page_coord[1]]])) + return cropped_page, page_coord, cont_page def early_page_for_num_of_column_classification(self,img_bin): if not self.ignore_page_extraction: self.logger.debug("enter early_page_for_num_of_column_classification") if self.input_binary: - img =np.copy(img_bin) - img = img.astype(np.uint8) + img = np.copy(img_bin).astype(np.uint8) else: img = self.imread() if not self.dir_in: self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(img, (5, 5), 0) - img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) @@ -1396,20 +1443,20 @@ class Eynollah: thresh = cv2.dilate(thresh, KERNEL, iterations=3) contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(contours)>0: - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt_size = np.array([cv2.contourArea(contours[j]) + for j in range(len(contours))]) cnt = contours[np.argmax(cnt_size)] - x, y, w, h = cv2.boundingRect(cnt) - box = [x, y, w, h] + box = cv2.boundingRect(cnt) else: box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, img) + cropped_page, page_coord = crop_image_inside_box(box, img) self.logger.debug("exit early_page_for_num_of_column_classification") else: img = self.imread() box = [0, 0, img.shape[1], img.shape[0]] - croped_page, page_coord = crop_image_inside_box(box, img) - return croped_page, page_coord + cropped_page, page_coord = crop_image_inside_box(box, img) + return cropped_page, page_coord def extract_text_regions_new(self, img, patches, cols): self.logger.debug("enter extract_text_regions") @@ -1420,84 +1467,33 @@ class Eynollah: self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) else: self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) - model_region = self.model_region_fl if patches else self.model_region_fl_np - if not patches: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - #img = img.astype(np.uint8) - prediction_regions2 = None - else: + if self.light_version: + pass + elif not patches: + img = otsu_copy_binary(img).astype(np.uint8) + prediction_regions = None + elif cols: + img = otsu_copy_binary(img).astype(np.uint8) if cols == 1: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 1000 / float(img_width_h)), 1000).astype(np.uint8) + elif cols == 2: + img = resize_image(img, int(img_height_h * 1300 / float(img_width_h)), 1300).astype(np.uint8) + elif cols == 3: + img = resize_image(img, int(img_height_h * 1600 / float(img_width_h)), 1600).astype(np.uint8) + elif cols == 4: + img = resize_image(img, int(img_height_h * 1900 / float(img_width_h)), 1900).astype(np.uint8) + elif cols == 5: + img = resize_image(img, int(img_height_h * 2200 / float(img_width_h)), 2200).astype(np.uint8) + else: + img = resize_image(img, int(img_height_h * 2500 / float(img_width_h)), 2500).astype(np.uint8) - img = resize_image(img, int(img_height_h * 1000 / float(img_width_h)), 1000) - img = img.astype(np.uint8) - - if cols == 2: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 1300 / float(img_width_h)), 1300) - img = img.astype(np.uint8) - - if cols == 3: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 1600 / float(img_width_h)), 1600) - img = img.astype(np.uint8) - - if cols == 4: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 1900 / float(img_width_h)), 1900) - img = img.astype(np.uint8) - - if cols == 5: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 2200 / float(img_width_h)), 2200) - img = img.astype(np.uint8) - - if cols >= 6: - if self.light_version: - pass - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 2500 / float(img_width_h)), 2500) - img = img.astype(np.uint8) - - marginal_of_patch_percent = 0.1 - - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=3) - - - ##prediction_regions = self.do_prediction(False, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent, n_batch_inference=3) - + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=0.1, n_batch_inference=3) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions - - + def extract_text_regions(self, img, patches, cols): self.logger.debug("enter extract_text_regions") img_height_h = img.shape[0] @@ -1507,92 +1503,51 @@ class Eynollah: self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) else: self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) - model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: img = otsu_copy_binary(img) img = img.astype(np.uint8) prediction_regions2 = None - else: + elif cols: if cols == 1: - img2 = otsu_copy_binary(img) - img2 = img2.astype(np.uint8) - img2 = resize_image(img2, int(img_height_h * 0.7), int(img_width_h * 0.7)) - marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) - prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) - - if cols == 2: - img2 = otsu_copy_binary(img) - img2 = img2.astype(np.uint8) - img2 = resize_image(img2, int(img_height_h * 0.4), int(img_width_h * 0.4)) - marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) - prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) - - elif cols > 2: - img2 = otsu_copy_binary(img) - img2 = img2.astype(np.uint8) - img2 = resize_image(img2, int(img_height_h * 0.3), int(img_width_h * 0.3)) - marginal_of_patch_percent = 0.1 - prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=marginal_of_patch_percent) - prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) - - if cols == 2: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - if img_width_h >= 2000: - img = resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)) - img = img.astype(np.uint8) + img_height_new = int(img_height_h * 0.7) + img_width_new = int(img_width_h * 0.7) + elif cols == 2: + img_height_new = int(img_height_h * 0.4) + img_width_new = int(img_width_h * 0.4) + else: + img_height_new = int(img_height_h * 0.3) + img_width_new = int(img_width_h * 0.3) + img2 = otsu_copy_binary(img) + img2 = img2.astype(np.uint8) + img2 = resize_image(img2, img_height_new, img_width_new) + prediction_regions2 = self.do_prediction(patches, img2, model_region, marginal_of_patch_percent=0.1) + prediction_regions2 = resize_image(prediction_regions2, img_height_h, img_width_h) + img = otsu_copy_binary(img).astype(np.uint8) if cols == 1: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 0.5), int(img_width_h * 0.5)) - img = img.astype(np.uint8) + img = resize_image(img, int(img_height_h * 0.5), int(img_width_h * 0.5)).astype(np.uint8) + elif cols == 2 and img_width_h >= 2000: + img = resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)).astype(np.uint8) + elif cols == 3 and ((self.scale_x == 1 and img_width_h > 3000) or + (self.scale_x != 1 and img_width_h > 2800)): + img = resize_image(img, 2800 * img_height_h // img_width_h, 2800).astype(np.uint8) + elif cols == 4 and ((self.scale_x == 1 and img_width_h > 4000) or + (self.scale_x != 1 and img_width_h > 3700)): + img = resize_image(img, 3700 * img_height_h // img_width_h, 3700).astype(np.uint8) + elif cols == 4: + img = resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)).astype(np.uint8) + elif cols == 5 and self.scale_x == 1 and img_width_h > 5000: + img = resize_image(img, int(img_height_h * 0.7), int(img_width_h * 0.7)).astype(np.uint8) + elif cols == 5: + img = resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)).astype(np.uint8) + elif img_width_h > 5600: + img = resize_image(img, 5600 * img_height_h // img_width_h, 5600).astype(np.uint8) + else: + img = resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)).astype(np.uint8) - if cols == 3: - if (self.scale_x == 1 and img_width_h > 3000) or (self.scale_x != 1 and img_width_h > 2800): - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img = resize_image(img, int(img_height_h * 2800 / float(img_width_h)), 2800) - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - - if cols == 4: - if (self.scale_x == 1 and img_width_h > 4000) or (self.scale_x != 1 and img_width_h > 3700): - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 3700 / float(img_width_h)), 3700) - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)) - - if cols == 5: - if self.scale_x == 1 and img_width_h > 5000: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 0.7), int(img_width_h * 0.7)) - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9) ) - - if cols >= 6: - if img_width_h > 5600: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 5600 / float(img_width_h)), 5600) - else: - img = otsu_copy_binary(img) - img = img.astype(np.uint8) - img= resize_image(img, int(img_height_h * 0.9), int(img_width_h * 0.9)) - - marginal_of_patch_percent = 0.1 - prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=marginal_of_patch_percent) + prediction_regions = self.do_prediction(patches, img, model_region, marginal_of_patch_percent=0.1) prediction_regions = resize_image(prediction_regions, img_height_h, img_width_h) self.logger.debug("exit extract_text_regions") return prediction_regions, prediction_regions2 @@ -1600,9 +1555,8 @@ class Eynollah: def get_slopes_and_deskew_new_light2(self, contours, contours_par, textline_mask_tot, image_page_rotated, boxes, slope_deskew): polygons_of_textlines = return_contours_of_interested_region(textline_mask_tot,1,0.00001) - - - M_main_tot = [cv2.moments(polygons_of_textlines[j]) for j in range(len(polygons_of_textlines))] + M_main_tot = [cv2.moments(polygons_of_textlines[j]) + for j in range(len(polygons_of_textlines))] cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] @@ -1612,18 +1566,16 @@ class Eynollah: all_box_coord =[] for index, con_region_ind in enumerate(contours_par): - results = [cv2.pointPolygonTest(con_region_ind, (cx_main_tot[ind], cy_main_tot[ind]), False) for ind in args_textlines ] + results = [cv2.pointPolygonTest(con_region_ind, (cx_main_tot[ind], cy_main_tot[ind]), False) + for ind in args_textlines ] results = np.array(results) - indexes_in = args_textlines[results==1] - textlines_ins = [polygons_of_textlines[ind] for ind in indexes_in] all_found_textline_polygons.append(textlines_ins) slopes.append(slope_deskew) _, crop_coor = crop_image_inside_box(boxes[index],image_page_rotated) - all_box_coord.append(crop_coor) return all_found_textline_polygons, boxes, contours, contours_par, all_box_coord, np.array(range(len(contours_par))), slopes @@ -1690,32 +1642,29 @@ class Eynollah: img_w = img_org.shape[1] img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) - prediction_textline = self.do_prediction(use_patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, - thresholding_for_artificial_class_in_light_version=self.textline_light) + prediction_textline = self.do_prediction( + use_patches, img, self.model_textline, + marginal_of_patch_percent=0.15, n_batch_inference=3, + thresholding_for_artificial_class_in_light_version=self.textline_light) #if not self.textline_light: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, self.model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 prediction_textline = resize_image(prediction_textline, img_h, img_w) - textline_mask_tot_ea_art = (prediction_textline[:,:]==2)*1 old_art = np.copy(textline_mask_tot_ea_art) - if not self.textline_light: textline_mask_tot_ea_art = textline_mask_tot_ea_art.astype('uint8') #textline_mask_tot_ea_art = cv2.dilate(textline_mask_tot_ea_art, KERNEL, iterations=1) - prediction_textline[:,:][textline_mask_tot_ea_art[:,:]==1]=2 textline_mask_tot_ea_lines = (prediction_textline[:,:]==1)*1 textline_mask_tot_ea_lines = textline_mask_tot_ea_lines.astype('uint8') - if not self.textline_light: textline_mask_tot_ea_lines = cv2.dilate(textline_mask_tot_ea_lines, KERNEL, iterations=1) prediction_textline[:,:][textline_mask_tot_ea_lines[:,:]==1]=1 - if not self.textline_light: prediction_textline[:,:][old_art[:,:]==1]=2 @@ -1723,7 +1672,8 @@ class Eynollah: prediction_textline_longshot_true_size = resize_image(prediction_textline_longshot, img_h, img_w) self.logger.debug('exit textline_contours') - return ((prediction_textline[:, :, 0]==1)*1).astype('uint8'), ((prediction_textline_longshot_true_size[:, :, 0]==1)*1).astype('uint8') + return ((prediction_textline[:, :, 0]==1).astype(np.uint8), + (prediction_textline_longshot_true_size[:, :, 0]==1).astype(np.uint8)) def do_work_of_slopes(self, q, poly, box_sub, boxes_per_process, textline_mask_tot, contours_per_process): @@ -1752,7 +1702,8 @@ class Eynollah: slope_corresponding_textregion = slope_biggest slopes_sub.append(slope_corresponding_textregion) - cnt_clean_rot = textline_contours_postprocessing(crop_img, slope_corresponding_textregion, contours_per_process[mv], boxes_per_process[mv]) + cnt_clean_rot = textline_contours_postprocessing( + crop_img, slope_corresponding_textregion, contours_per_process[mv], boxes_per_process[mv]) poly_sub.append(cnt_clean_rot) boxes_sub_new.append(boxes_per_process[mv]) @@ -1782,55 +1733,41 @@ class Eynollah: elif num_col_classifier == 6: img_w_new = 2500 img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) - img_resized = resize_image(img,img_h_new, img_w_new ) - - if not self.dir_in: self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light_only_images_extraction) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region) prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) - image_page, page_coord, cont_page = self.extract_page() - prediction_regions_org = prediction_regions_org[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] - - prediction_regions_org=prediction_regions_org[:,:,0] mask_lines_only = (prediction_regions_org[:,:] ==3)*1 - mask_texts_only = (prediction_regions_org[:,:] ==1)*1 - mask_images_only=(prediction_regions_org[:,:] ==2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = textline_con_fil = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) - + polygons_lines_xml = textline_con_fil = filter_contours_area_of_image( + mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) - polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) text_regions_p_true = np.zeros(prediction_regions_org.shape) - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 - - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) - - + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts=polygons_of_only_texts, color=(1,1,1)) text_regions_p_true[text_regions_p_true.shape[0]-15:text_regions_p_true.shape[0], :] = 0 text_regions_p_true[:, text_regions_p_true.shape[1]-15:text_regions_p_true.shape[1]] = 0 ##polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.0001) polygons_of_images = return_contours_of_interested_region(text_regions_p_true, 2, 0.001) - image_boundary_of_doc = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) ###image_boundary_of_doc[:6, :] = 1 @@ -1839,14 +1776,13 @@ class Eynollah: ###image_boundary_of_doc[:, :6] = 1 ###image_boundary_of_doc[:, text_regions_p_true.shape[1]-6:text_regions_p_true.shape[1]] = 1 - polygons_of_images_fin = [] for ploy_img_ind in polygons_of_images: """ test_poly_image = np.zeros((text_regions_p_true.shape[0], text_regions_p_true.shape[1])) - test_poly_image = cv2.fillPoly(test_poly_image, pts = [ploy_img_ind], color=(1,1,1)) + test_poly_image = cv2.fillPoly(test_poly_image, pts=[ploy_img_ind], color=(1,1,1)) - test_poly_image = test_poly_image[:,:] + image_boundary_of_doc[:,:] + test_poly_image = test_poly_image + image_boundary_of_doc test_poly_image_intersected_area = ( test_poly_image[:,:]==2 )*1 test_poly_image_intersected_area = test_poly_image_intersected_area.sum() @@ -1854,22 +1790,30 @@ class Eynollah: if test_poly_image_intersected_area==0: ##polygons_of_images_fin.append(ploy_img_ind) - x, y, w, h = cv2.boundingRect(ploy_img_ind) - box = [x, y, w, h] + box = cv2.boundingRect(ploy_img_ind) _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) - #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - - polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) + # cont_page.append(np.array([[page_coord[2], page_coord[0]], + # [page_coord[3], page_coord[0]], + # [page_coord[3], page_coord[1]], + # [page_coord[2], page_coord[1]]])) + polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], + [page_coord_img[3], page_coord_img[0]], + [page_coord_img[3], page_coord_img[1]], + [page_coord_img[2], page_coord_img[1]]]) ) """ - x, y, w, h = cv2.boundingRect(ploy_img_ind) + box = x, y, w, h = cv2.boundingRect(ploy_img_ind) if h < 150 or w < 150: pass else: - box = [x, y, w, h] _, page_coord_img = crop_image_inside_box(box, text_regions_p_true) - #cont_page.append(np.array([[page_coord[2], page_coord[0]], [page_coord[3], page_coord[0]], [page_coord[3], page_coord[1]], [page_coord[2], page_coord[1]]])) - - polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], [page_coord_img[3], page_coord_img[0]], [page_coord_img[3], page_coord_img[1]], [page_coord_img[2], page_coord_img[1]]]) ) + # cont_page.append(np.array([[page_coord[2], page_coord[0]], + # [page_coord[3], page_coord[0]], + # [page_coord[3], page_coord[1]], + # [page_coord[2], page_coord[1]]])) + polygons_of_images_fin.append(np.array([[page_coord_img[2], page_coord_img[0]], + [page_coord_img[3], page_coord_img[0]], + [page_coord_img[3], page_coord_img[1]], + [page_coord_img[2], page_coord_img[1]]])) self.logger.debug("exit get_regions_extract_images_only") return text_regions_p_true, erosion_hurts, polygons_lines_xml, polygons_of_images_fin, image_page, page_coord, cont_page @@ -1883,34 +1827,24 @@ class Eynollah: img_width_h = img_org.shape[1] #model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) - #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: img_w_new = 1000 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) - elif num_col_classifier == 2: img_w_new = 1500#1500 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) - elif num_col_classifier == 3: img_w_new = 2000 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) - elif num_col_classifier == 4: img_w_new = 2500 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) elif num_col_classifier == 5: img_w_new = 3000 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) else: img_w_new = 4000 - img_h_new = int(img_org.shape[0] / float(img_org.shape[1]) * img_w_new) + img_h_new = img_w_new * img_org.shape[0] // img_org.shape[1] img_resized = resize_image(img,img_h_new, img_w_new ) t_bin = time.time() - #if (not self.input_binary) or self.full_layout: #if self.input_binary: #img_bin = np.copy(img_resized) @@ -1935,12 +1869,8 @@ class Eynollah: if not self.dir_in: self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) - prediction_bin=prediction_bin[:,:,0] - prediction_bin = (prediction_bin[:,:]==0)*1 - prediction_bin = prediction_bin*255 - - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - + prediction_bin = 255 * (prediction_bin[:,:,0] == 0) + prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) prediction_bin = prediction_bin.astype(np.uint16) #img= np.copy(prediction_bin) img_bin = np.copy(prediction_bin) @@ -1951,11 +1881,8 @@ class Eynollah: ###textline_mask_tot_ea = self.run_textline(img_bin) self.logger.debug("detecting textlines on %s with %d colors", str(img_resized.shape), len(np.unique(img_resized))) textline_mask_tot_ea = self.run_textline(img_resized, num_col_classifier) - - textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_height_h, img_width_h ) - #print(self.image_org.shape) #cv2.imwrite('out_13.png', self.image_page_org_size) @@ -1979,7 +1906,9 @@ class Eynollah: prediction_regions_page = self.do_prediction_new_concept( False, self.image_page_org_size, self.model_region_1_2, n_batch_inference=1, thresholding_for_artificial_class_in_light_version=True) - prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page + ys = slice(*self.page_coord[0:2]) + xs = slice(*self.page_coord[2:4]) + prediction_regions_org[ys, xs] = prediction_regions_page else: new_h = (900+ (num_col_classifier-3)*100) img_resized = resize_image(img_bin, int(new_h * img_bin.shape[0] /img_bin.shape[1]), new_h) @@ -1989,26 +1918,16 @@ class Eynollah: True, img_resized, self.model_region_1_2, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ###prediction_regions_org = self.do_prediction(True, img_bin, self.model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) - #print("inside 3 ", time.time()-t_in) - #plt.imshow(prediction_regions_org[:,:,0]) #plt.show() - prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) - img_bin = resize_image(img_bin,img_height_h, img_width_h ) - prediction_regions_org=prediction_regions_org[:,:,0] - mask_lines_only = (prediction_regions_org[:,:] ==3)*1 - - - mask_texts_only = (prediction_regions_org[:,:] ==1)*1 - mask_texts_only = mask_texts_only.astype('uint8') ##if num_col_classifier == 1 or num_col_classifier == 2: @@ -2016,57 +1935,39 @@ class Eynollah: ##mask_texts_only = cv2.dilate(mask_texts_only, KERNEL, iterations=1) mask_texts_only = cv2.dilate(mask_texts_only, kernel=np.ones((2,2), np.uint8), iterations=1) - - mask_images_only=(prediction_regions_org[:,:] ==2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - - test_khat = np.zeros(prediction_regions_org.shape) - - test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) - - + test_khat = cv2.fillPoly(test_khat, pts=polygons_lines_xml, color=(1,1,1)) + #plt.imshow(test_khat[:,:]) #plt.show() - #for jv in range(1): #print(jv, hir_lines_xml[0][232][3]) #test_khat = np.zeros(prediction_regions_org.shape) - #test_khat = cv2.fillPoly(test_khat, pts = [polygons_lines_xml[232]], color=(1,1,1)) - - #plt.imshow(test_khat[:,:]) #plt.show() - - polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) - + polygons_lines_xml = filter_contours_area_of_image( + mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) test_khat = np.zeros(prediction_regions_org.shape) - test_khat = cv2.fillPoly(test_khat, pts = polygons_lines_xml, color=(1,1,1)) - #plt.imshow(test_khat[:,:]) #plt.show() #sys.exit() polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) - ##polygons_of_only_texts = self.dilate_textregions_contours(polygons_of_only_texts) - - polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) text_regions_p_true = np.zeros(prediction_regions_org.shape) - - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) + text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts=polygons_of_only_lines, color=(3,3,3)) text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) #plt.imshow(textline_mask_tot_ea) @@ -2107,14 +2008,10 @@ class Eynollah: mask_zeros_y = (prediction_regions_org_y[:,:]==0)*1 ##img_only_regions_with_sep = ( (prediction_regions_org_y[:,:] != 3) & (prediction_regions_org_y[:,:] != 0) )*1 - img_only_regions_with_sep = ( prediction_regions_org_y[:,:] == 1 )*1 - img_only_regions_with_sep = img_only_regions_with_sep.astype(np.uint8) - + img_only_regions_with_sep = (prediction_regions_org_y == 1).astype(np.uint8) try: img_only_regions = cv2.erode(img_only_regions_with_sep[:,:], KERNEL, iterations=20) - _, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) - img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1]*(1.2 if is_image_enhanced else 1))) prediction_regions_org = self.do_prediction(True, img, self.model_region) @@ -2122,8 +2019,7 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] prediction_regions_org[(prediction_regions_org[:,:]==1) & (mask_zeros_y[:,:]==1)]=0 - - + if not self.dir_in: self.model_region_p2, _ = self.start_new_session_and_model(self.model_region_dir_p2) @@ -2132,30 +2028,23 @@ class Eynollah: prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, marginal_of_patch_percent=0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) - mask_zeros2 = (prediction_regions_org2[:,:,0] == 0) mask_lines2 = (prediction_regions_org2[:,:,0] == 3) text_sume_early = (prediction_regions_org[:,:] == 1).sum() prediction_regions_org_copy = np.copy(prediction_regions_org) prediction_regions_org_copy[(prediction_regions_org_copy[:,:]==1) & (mask_zeros2[:,:]==1)] = 0 text_sume_second = ((prediction_regions_org_copy[:,:]==1)*1).sum() - - rate_two_models = text_sume_second / float(text_sume_early) * 100 + rate_two_models = 100. * text_sume_second / text_sume_early self.logger.info("ratio_of_two_models: %s", rate_two_models) if not(is_image_enhanced and rate_two_models < RATIO_OF_TWO_MODEL_THRESHOLD): prediction_regions_org = np.copy(prediction_regions_org_copy) - - prediction_regions_org[(mask_lines2[:,:]==1) & (prediction_regions_org[:,:]==0)]=3 mask_lines_only=(prediction_regions_org[:,:]==3)*1 prediction_regions_org = cv2.erode(prediction_regions_org[:,:], KERNEL, iterations=2) - - prediction_regions_org = cv2.dilate(prediction_regions_org[:,:], KERNEL, iterations=2) - - + if rate_two_models<=40: if self.input_binary: prediction_bin = np.copy(img_org) @@ -2164,19 +2053,14 @@ class Eynollah: self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) - - prediction_bin=prediction_bin[:,:,0] - prediction_bin = (prediction_bin[:,:]==0)*1 - prediction_bin = prediction_bin*255 - - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) + prediction_bin = 255 * (prediction_bin[:,:,0]==0) + prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) if not self.dir_in: self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1 ratio_x=1 - img = resize_image(prediction_bin, int(img_org.shape[0]*ratio_y), int(img_org.shape[1]*ratio_x)) prediction_regions_org = self.do_prediction(True, img, self.model_region) @@ -2188,10 +2072,9 @@ class Eynollah: mask_texts_only=(prediction_regions_org[:,:]==1)*1 mask_images_only=(prediction_regions_org[:,:]==2)*1 - - polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) + polygons_lines_xml = filter_contours_area_of_image( + mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only, 1, 0.00001) polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only, 1, 0.00001) @@ -2205,7 +2088,6 @@ class Eynollah: self.logger.debug("exit get_regions_from_xy_2models") return text_regions_p_true, erosion_hurts, polygons_lines_xml except: - if self.input_binary: prediction_bin = np.copy(img_org) @@ -2213,14 +2095,8 @@ class Eynollah: self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) - prediction_bin=prediction_bin[:,:,0] - - prediction_bin = (prediction_bin[:,:]==0)*1 - - prediction_bin = prediction_bin*255 - - prediction_bin =np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - + prediction_bin = 255 * (prediction_bin[:,:,0]==0) + prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) if not self.dir_in: self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) @@ -2248,49 +2124,53 @@ class Eynollah: #prediction_regions_org[(prediction_regions_org[:,:] == 1) & (mask_zeros_y[:,:] == 1)]=0 - mask_lines_only = (prediction_regions_org[:,:] ==3)*1 - - mask_texts_only = (prediction_regions_org[:,:] ==1)*1 - - mask_images_only=(prediction_regions_org[:,:] ==2)*1 + mask_lines_only = (prediction_regions_org == 3)*1 + mask_texts_only = (prediction_regions_org == 1)*1 + mask_images_only= (prediction_regions_org == 2)*1 polygons_lines_xml, hir_lines_xml = return_contours_of_image(mask_lines_only) - polygons_lines_xml = filter_contours_area_of_image(mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) - + polygons_lines_xml = filter_contours_area_of_image( + mask_lines_only, polygons_lines_xml, hir_lines_xml, max_area=1, min_area=0.00001) polygons_of_only_texts = return_contours_of_interested_region(mask_texts_only,1,0.00001) - polygons_of_only_lines = return_contours_of_interested_region(mask_lines_only,1,0.00001) - text_regions_p_true = np.zeros(prediction_regions_org.shape) - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_lines, color=(3,3,3)) text_regions_p_true[:,:][mask_images_only[:,:] == 1] = 2 - text_regions_p_true = cv2.fillPoly(text_regions_p_true, pts = polygons_of_only_texts, color=(1,1,1)) erosion_hurts = True self.logger.debug("exit get_regions_from_xy_2models") return text_regions_p_true, erosion_hurts, polygons_lines_xml - def do_order_of_regions_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): + def do_order_of_regions_full_layout( + self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): + self.logger.debug("enter do_order_of_regions_full_layout") - cx_text_only, cy_text_only, x_min_text_only, _, _, _, y_cor_x_min_main = find_new_features_of_contours(contours_only_text_parent) - cx_text_only_h, cy_text_only_h, x_min_text_only_h, _, _, _, y_cor_x_min_main_h = find_new_features_of_contours(contours_only_text_parent_h) + boxes = np.array(boxes, dtype=int) # to be on the safe side + cx_text_only, cy_text_only, x_min_text_only, _, _, _, y_cor_x_min_main = find_new_features_of_contours( + contours_only_text_parent) + cx_text_only_h, cy_text_only_h, x_min_text_only_h, _, _, _, y_cor_x_min_main_h = find_new_features_of_contours( + contours_only_text_parent_h) try: arg_text_con = [] for ii in range(len(cx_text_only)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if (x_min_text_only[ii] + 80) >= boxes[jj][0] and (x_min_text_only[ii] + 80) < boxes[jj][1] and y_cor_x_min_main[ii] >= boxes[jj][2] and y_cor_x_min_main[ii] < boxes[jj][3]: + if (x_min_text_only[ii] + 80 >= boxes[jj][0] and + x_min_text_only[ii] + 80 < boxes[jj][1] and + y_cor_x_min_main[ii] >= boxes[jj][2] and + y_cor_x_min_main[ii] < boxes[jj][3]): arg_text_con.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + + (cy_text_only[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) @@ -2298,12 +2178,17 @@ class Eynollah: for ii in range(len(cx_text_only_h)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if (x_min_text_only_h[ii] + 80) >= boxes[jj][0] and (x_min_text_only_h[ii] + 80) < boxes[jj][1] and y_cor_x_min_main_h[ii] >= boxes[jj][2] and y_cor_x_min_main_h[ii] < boxes[jj][3]: + if (x_min_text_only_h[ii] + 80 >= boxes[jj][0] and + x_min_text_only_h[ii] + 80 < boxes[jj][1] and + y_cor_x_min_main_h[ii] >= boxes[jj][2] and + y_cor_x_min_main_h[ii] < boxes[jj][3]): arg_text_con_h.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con_h.append(ind_min) args_contours_h = np.array(range(len(arg_text_con_h))) @@ -2315,7 +2200,8 @@ class Eynollah: order_of_texts_tot = [] id_of_texts_tot = [] for iij in range(len(boxes)): - + ys = slice(*boxes[iij][2:4]) + xs = slice(*boxes[iij][0:2]) args_contours_box = args_contours[np.array(arg_text_con) == iij] args_contours_box_h = args_contours_h[np.array(arg_text_con_h) == iij] con_inter_box = [] @@ -2327,9 +2213,12 @@ class Eynollah: for box in args_contours_box_h: con_inter_box_h.append(contours_only_text_parent_h[box]) - indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions(textline_mask_tot[int(boxes[iij][2]) : int(boxes[iij][3]), int(boxes[iij][0]) : int(boxes[iij][1])], con_inter_box, con_inter_box_h, boxes[iij][2]) + indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions( + textline_mask_tot[ys, xs], con_inter_box, con_inter_box_h, boxes[iij][2]) - order_of_texts, id_of_texts = order_and_id_of_texts(con_inter_box, con_inter_box_h, matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) + order_of_texts, id_of_texts = order_and_id_of_texts( + con_inter_box, con_inter_box_h, + matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) indexes_sorted_main = np.array(indexes_sorted)[np.array(kind_of_texts_sorted) == 1] indexes_by_type_main = np.array(index_by_kind_sorted)[np.array(kind_of_texts_sorted) == 1] @@ -2338,11 +2227,13 @@ class Eynollah: for zahler, _ in enumerate(args_contours_box): arg_order_v = indexes_sorted_main[zahler] - order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for zahler, _ in enumerate(args_contours_box_h): arg_order_v = indexes_sorted_head[zahler] - order_by_con_head[args_contours_box_h[indexes_by_type_head[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_head[args_contours_box_h[indexes_by_type_head[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for jji in range(len(id_of_texts)): order_of_texts_tot.append(order_of_texts[jji] + ref_point) @@ -2366,17 +2257,22 @@ class Eynollah: for ii in range(len(cx_text_only)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located + if (cx_text_only[ii] >= boxes[jj][0] and + cx_text_only[ii] < boxes[jj][1] and + cy_text_only[ii] >= boxes[jj][2] and + cy_text_only[ii] < boxes[jj][3]): + # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + + (cy_text_only[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) - order_by_con_main = np.zeros(len(arg_text_con)) ############################# head @@ -2385,22 +2281,29 @@ class Eynollah: for ii in range(len(cx_text_only_h)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if cx_text_only_h[ii] >= boxes[jj][0] and cx_text_only_h[ii] < boxes[jj][1] and cy_text_only_h[ii] >= boxes[jj][2] and cy_text_only_h[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located + if (cx_text_only_h[ii] >= boxes[jj][0] and + cx_text_only_h[ii] < boxes[jj][1] and + cy_text_only_h[ii] >= boxes[jj][2] and + cy_text_only_h[ii] < boxes[jj][3]): + # this is valid if the center of region identify in which box it is located arg_text_con_h.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only_h[ii] - boxes[jj][1]) ** 2 + + (cy_text_only_h[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con_h.append(ind_min) args_contours_h = np.array(range(len(arg_text_con_h))) - order_by_con_head = np.zeros(len(arg_text_con_h)) ref_point = 0 order_of_texts_tot = [] id_of_texts_tot = [] for iij, _ in enumerate(boxes): + ys = slice(*boxes[iij][2:4]) + xs = slice(*boxes[iij][0:2]) args_contours_box = args_contours[np.array(arg_text_con) == iij] args_contours_box_h = args_contours_h[np.array(arg_text_con_h) == iij] con_inter_box = [] @@ -2412,9 +2315,12 @@ class Eynollah: for box in args_contours_box_h: con_inter_box_h.append(contours_only_text_parent_h[box]) - indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions(textline_mask_tot[int(boxes[iij][2]) : int(boxes[iij][3]), int(boxes[iij][0]) : int(boxes[iij][1])], con_inter_box, con_inter_box_h, boxes[iij][2]) + indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions( + textline_mask_tot[ys, xs], con_inter_box, con_inter_box_h, boxes[iij][2]) - order_of_texts, id_of_texts = order_and_id_of_texts(con_inter_box, con_inter_box_h, matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) + order_of_texts, id_of_texts = order_and_id_of_texts( + con_inter_box, con_inter_box_h, + matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) indexes_sorted_main = np.array(indexes_sorted)[np.array(kind_of_texts_sorted) == 1] indexes_by_type_main = np.array(index_by_kind_sorted)[np.array(kind_of_texts_sorted) == 1] @@ -2423,11 +2329,13 @@ class Eynollah: for zahler, _ in enumerate(args_contours_box): arg_order_v = indexes_sorted_main[zahler] - order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for zahler, _ in enumerate(args_contours_box_h): arg_order_v = indexes_sorted_head[zahler] - order_by_con_head[args_contours_box_h[indexes_by_type_head[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_head[args_contours_box_h[indexes_by_type_head[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for jji, _ in enumerate(id_of_texts): order_of_texts_tot.append(order_of_texts[jji] + ref_point) @@ -2448,21 +2356,30 @@ class Eynollah: self.logger.debug("exit do_order_of_regions_full_layout") return order_text_new, id_of_texts_tot - def do_order_of_regions_no_full_layout(self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): + def do_order_of_regions_no_full_layout( + self, contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot): + self.logger.debug("enter do_order_of_regions_no_full_layout") - cx_text_only, cy_text_only, x_min_text_only, _, _, _, y_cor_x_min_main = find_new_features_of_contours(contours_only_text_parent) + boxes = np.array(boxes, dtype=int) # to be on the safe side + cx_text_only, cy_text_only, x_min_text_only, _, _, _, y_cor_x_min_main = find_new_features_of_contours( + contours_only_text_parent) try: arg_text_con = [] for ii in range(len(cx_text_only)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if (x_min_text_only[ii] + 80) >= boxes[jj][0] and (x_min_text_only[ii] + 80) < boxes[jj][1] and y_cor_x_min_main[ii] >= boxes[jj][2] and y_cor_x_min_main[ii] < boxes[jj][3]: + if (x_min_text_only[ii] + 80 >= boxes[jj][0] and + x_min_text_only[ii] + 80 < boxes[jj][1] and + y_cor_x_min_main[ii] >= boxes[jj][2] and + y_cor_x_min_main[ii] < boxes[jj][3]): arg_text_con.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + + (cy_text_only[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) @@ -2472,22 +2389,28 @@ class Eynollah: order_of_texts_tot = [] id_of_texts_tot = [] for iij in range(len(boxes)): + ys = slice(*boxes[iij][2:4]) + xs = slice(*boxes[iij][0:2]) args_contours_box = args_contours[np.array(arg_text_con) == iij] con_inter_box = [] con_inter_box_h = [] for i in range(len(args_contours_box)): con_inter_box.append(contours_only_text_parent[args_contours_box[i]]) - indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions(textline_mask_tot[int(boxes[iij][2]) : int(boxes[iij][3]), int(boxes[iij][0]) : int(boxes[iij][1])], con_inter_box, con_inter_box_h, boxes[iij][2]) + indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions( + textline_mask_tot[ys, xs], con_inter_box, con_inter_box_h, boxes[iij][2]) - order_of_texts, id_of_texts = order_and_id_of_texts(con_inter_box, con_inter_box_h, matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) + order_of_texts, id_of_texts = order_and_id_of_texts( + con_inter_box, con_inter_box_h, + matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) indexes_sorted_main = np.array(indexes_sorted)[np.array(kind_of_texts_sorted) == 1] indexes_by_type_main = np.array(index_by_kind_sorted)[np.array(kind_of_texts_sorted) == 1] for zahler, _ in enumerate(args_contours_box): arg_order_v = indexes_sorted_main[zahler] - order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for jji, _ in enumerate(id_of_texts): order_of_texts_tot.append(order_of_texts[jji] + ref_point) @@ -2508,39 +2431,49 @@ class Eynollah: for ii in range(len(cx_text_only)): check_if_textregion_located_in_a_box = False for jj in range(len(boxes)): - if cx_text_only[ii] >= boxes[jj][0] and cx_text_only[ii] < boxes[jj][1] and cy_text_only[ii] >= boxes[jj][2] and cy_text_only[ii] < boxes[jj][3]: # this is valid if the center of region identify in which box it is located + if (cx_text_only[ii] >= boxes[jj][0] and + cx_text_only[ii] < boxes[jj][1] and + cy_text_only[ii] >= boxes[jj][2] and + cy_text_only[ii] < boxes[jj][3]): + # this is valid if the center of region identify in which box it is located arg_text_con.append(jj) check_if_textregion_located_in_a_box = True break if not check_if_textregion_located_in_a_box: - dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + (cy_text_only[ii] - boxes[jj][2]) ** 2) for jj in range(len(boxes))] + dists_tr_from_box = [math.sqrt((cx_text_only[ii] - boxes[jj][1]) ** 2 + + (cy_text_only[ii] - boxes[jj][2]) ** 2) + for jj in range(len(boxes))] ind_min = np.argmin(dists_tr_from_box) arg_text_con.append(ind_min) args_contours = np.array(range(len(arg_text_con))) - order_by_con_main = np.zeros(len(arg_text_con)) ref_point = 0 order_of_texts_tot = [] id_of_texts_tot = [] for iij in range(len(boxes)): + ys = slice(*boxes[iij][2:4]) + xs = slice(*boxes[iij][0:2]) args_contours_box = args_contours[np.array(arg_text_con) == iij] con_inter_box = [] con_inter_box_h = [] - for i in range(len(args_contours_box)): con_inter_box.append(contours_only_text_parent[args_contours_box[i]]) - indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions(textline_mask_tot[int(boxes[iij][2]) : int(boxes[iij][3]), int(boxes[iij][0]) : int(boxes[iij][1])], con_inter_box, con_inter_box_h, boxes[iij][2]) + indexes_sorted, matrix_of_orders, kind_of_texts_sorted, index_by_kind_sorted = order_of_regions( + textline_mask_tot[ys, xs], con_inter_box, con_inter_box_h, boxes[iij][2]) - order_of_texts, id_of_texts = order_and_id_of_texts(con_inter_box, con_inter_box_h, matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) + order_of_texts, id_of_texts = order_and_id_of_texts( + con_inter_box, con_inter_box_h, + matrix_of_orders, indexes_sorted, index_by_kind_sorted, kind_of_texts_sorted, ref_point) indexes_sorted_main = np.array(indexes_sorted)[np.array(kind_of_texts_sorted) == 1] indexes_by_type_main = np.array(index_by_kind_sorted)[np.array(kind_of_texts_sorted) == 1] for zahler, _ in enumerate(args_contours_box): arg_order_v = indexes_sorted_main[zahler] - order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = np.where(indexes_sorted == arg_order_v)[0][0] + ref_point + order_by_con_main[args_contours_box[indexes_by_type_main[zahler]]] = \ + np.where(indexes_sorted == arg_order_v)[0][0] + ref_point for jji, _ in enumerate(id_of_texts): order_of_texts_tot.append(order_of_texts[jji] + ref_point) @@ -2558,10 +2491,13 @@ class Eynollah: self.logger.debug("exit do_order_of_regions_no_full_layout") return order_text_new, id_of_texts_tot - def check_iou_of_bounding_box_and_contour_for_tables(self, layout, table_prediction_early, pixel_tabel, num_col_classifier): + + def check_iou_of_bounding_box_and_contour_for_tables( + self, layout, table_prediction_early, pixel_table, num_col_classifier): + layout_org = np.copy(layout) - layout_org[:,:,0][layout_org[:,:,0]==pixel_tabel] = 0 - layout = (layout[:,:,0]==pixel_tabel)*1 + layout_org[:,:,0][layout_org[:,:,0]==pixel_table] = 0 + layout = (layout[:,:,0]==pixel_table)*1 layout =np.repeat(layout[:, :, np.newaxis], 3, axis=2) layout = layout.astype(np.uint8) @@ -2569,18 +2505,17 @@ class Eynollah: _, thresh = cv2.threshold(imgray, 0, 255, 0) contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - cnt_size = np.array([cv2.contourArea(contours[j]) for j in range(len(contours))]) + cnt_size = np.array([cv2.contourArea(contours[j]) + for j in range(len(contours))]) contours_new = [] for i in range(len(contours)): x, y, w, h = cv2.boundingRect(contours[i]) iou = cnt_size[i] /float(w*h) *100 - if iou<80: layout_contour = np.zeros((layout_org.shape[0], layout_org.shape[1])) layout_contour= cv2.fillPoly(layout_contour,pts=[contours[i]] ,color=(1,1,1)) - - + layout_contour_sum = layout_contour.sum(axis=0) layout_contour_sum_diff = np.diff(layout_contour_sum) layout_contour_sum_diff= np.abs(layout_contour_sum_diff) @@ -2607,65 +2542,77 @@ class Eynollah: contours_new.append(contours_sep[ji]) if num_col_classifier>=2: only_recent_contour_image = np.zeros((layout.shape[0],layout.shape[1])) - only_recent_contour_image= cv2.fillPoly(only_recent_contour_image,pts=[contours_sep[ji]] ,color=(1,1,1)) - table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] - iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 + only_recent_contour_image= cv2.fillPoly(only_recent_contour_image, pts=[contours_sep[ji]], color=(1,1,1)) + table_pixels_masked_from_early_pre = only_recent_contour_image * table_prediction_early + iou_in = 100. * table_pixels_masked_from_early_pre.sum() / only_recent_contour_image.sum() #print(iou_in,'iou_in_in1') if iou_in>30: - layout_org= cv2.fillPoly(layout_org,pts=[contours_sep[ji]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + layout_org= cv2.fillPoly(layout_org, pts=[contours_sep[ji]], color=3 * (pixel_table,)) else: pass else: - - layout_org= cv2.fillPoly(layout_org,pts=[contours_sep[ji]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) - + layout_org= cv2.fillPoly(layout_org, pts=[contours_sep[ji]], color=3 * (pixel_table,)) else: contours_new.append(contours[i]) if num_col_classifier>=2: only_recent_contour_image = np.zeros((layout.shape[0],layout.shape[1])) only_recent_contour_image= cv2.fillPoly(only_recent_contour_image,pts=[contours[i]] ,color=(1,1,1)) - table_pixels_masked_from_early_pre = only_recent_contour_image[:,:]*table_prediction_early[:,:] - iou_in = table_pixels_masked_from_early_pre.sum() /float(only_recent_contour_image.sum()) *100 + table_pixels_masked_from_early_pre = only_recent_contour_image * table_prediction_early + iou_in = 100. * table_pixels_masked_from_early_pre.sum() / only_recent_contour_image.sum() #print(iou_in,'iou_in') if iou_in>30: - layout_org= cv2.fillPoly(layout_org,pts=[contours[i]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + layout_org= cv2.fillPoly(layout_org, pts=[contours[i]], color=3 * (pixel_table,)) else: pass else: - layout_org= cv2.fillPoly(layout_org,pts=[contours[i]] ,color=(pixel_tabel,pixel_tabel,pixel_tabel)) + layout_org= cv2.fillPoly(layout_org, pts=[contours[i]], color=3 * (pixel_table,)) return layout_org, contours_new - def delete_separator_around(self,spliter_y,peaks_neg,image_by_region, pixel_line, pixel_table): + + def delete_separator_around(self, spliter_y,peaks_neg,image_by_region, pixel_line, pixel_table): # format of subboxes: box=[x1, x2 , y1, y2] pix_del = 100 if len(image_by_region.shape)==3: for i in range(len(spliter_y)-1): for j in range(1,len(peaks_neg[i])-1): - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0]==pixel_line ]=0 - image_by_region[spliter_y[i]:spliter_y[i+1],peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,1]==pixel_line ]=0 - image_by_region[spliter_y[i]:spliter_y[i+1],peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,2]==pixel_line ]=0 + ys = slice(int(spliter_y[i]), + int(spliter_y[i+1])) + xs = slice(peaks_neg[i][j] - pix_del, + peaks_neg[i][j] + pix_del) + image_by_region[ys,xs,0][image_by_region[ys,xs,0]==pixel_line] = 0 + image_by_region[ys,xs,0][image_by_region[ys,xs,1]==pixel_line] = 0 + image_by_region[ys,xs,0][image_by_region[ys,xs,2]==pixel_line] = 0 - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0]==pixel_table ]=0 - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,1]==pixel_table ]=0 - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,0][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del,2]==pixel_table ]=0 + image_by_region[ys,xs,0][image_by_region[ys,xs,0]==pixel_table] = 0 + image_by_region[ys,xs,0][image_by_region[ys,xs,1]==pixel_table] = 0 + image_by_region[ys,xs,0][image_by_region[ys,xs,2]==pixel_table] = 0 else: for i in range(len(spliter_y)-1): for j in range(1,len(peaks_neg[i])-1): - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del]==pixel_line ]=0 - - image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del][image_by_region[int(spliter_y[i]):int(spliter_y[i+1]),peaks_neg[i][j]-pix_del:peaks_neg[i][j]+pix_del]==pixel_table ]=0 + ys = slice(int(spliter_y[i]), + int(spliter_y[i+1])) + xs = slice(peaks_neg[i][j] - pix_del, + peaks_neg[i][j] + pix_del) + image_by_region[ys,xs][image_by_region[ys,xs]==pixel_line] = 0 + image_by_region[ys,xs][image_by_region[ys,xs]==pixel_table] = 0 return image_by_region - def add_tables_heuristic_to_layout(self, image_regions_eraly_p,boxes, slope_mean_hor, spliter_y,peaks_neg_tot, image_revised, num_col_classifier, min_area, pixel_line): + + def add_tables_heuristic_to_layout( + self, image_regions_eraly_p, boxes, + slope_mean_hor, spliter_y, peaks_neg_tot, image_revised, + num_col_classifier, min_area, pixel_line): + pixel_table =10 image_revised_1 = self.delete_separator_around(spliter_y, peaks_neg_tot, image_revised, pixel_line, pixel_table) try: image_revised_1[:,:30][image_revised_1[:,:30]==pixel_line] = 0 - image_revised_1[:,image_revised_1.shape[1]-30:][image_revised_1[:,image_revised_1.shape[1]-30:]==pixel_line] = 0 + image_revised_1[:,-30:][image_revised_1[:,-30:]==pixel_line] = 0 except: pass + boxes = np.array(boxes, dtype=int) # to be on the safe side img_comm_e = np.zeros(image_revised_1.shape) img_comm = np.repeat(img_comm_e[:, :, np.newaxis], 3, axis=2) @@ -2690,7 +2637,9 @@ class Eynollah: if not self.isNaN(slope_mean_hor): image_revised_last = np.zeros((image_regions_eraly_p.shape[0], image_regions_eraly_p.shape[1],3)) for i in range(len(boxes)): - image_box=img_comm[int(boxes[i][2]):int(boxes[i][3]),int(boxes[i][0]):int(boxes[i][1]),:] + box_ys = slice(*boxes[i][2:4]) + box_xs = slice(*boxes[i][0:2]) + image_box = img_comm[box_ys, box_xs] try: image_box_tabels_1=(image_box[:,:,0]==pixel_table)*1 contours_tab,_=return_contours_of_image(image_box_tabels_1) @@ -2753,17 +2702,17 @@ class Eynollah: for ii in range(len(y_up_tabs)): image_box[y_up_tabs[ii]:y_down_tabs[ii],:,0]=pixel_table - image_revised_last[int(boxes[i][2]):int(boxes[i][3]),int(boxes[i][0]):int(boxes[i][1]),:]=image_box[:,:,:] + image_revised_last[box_ys, box_xs] = image_box else: for i in range(len(boxes)): - - image_box=img_comm[int(boxes[i][2]):int(boxes[i][3]),int(boxes[i][0]):int(boxes[i][1]),:] - image_revised_last[int(boxes[i][2]):int(boxes[i][3]),int(boxes[i][0]):int(boxes[i][1]),:]=image_box[:,:,:] + box_ys = slice(*boxes[i][2:4]) + box_xs = slice(*boxes[i][0:2]) + image_box = img_comm[box_ys, box_xs] + image_revised_last[box_ys, box_xs] = image_box if num_col_classifier==1: - img_tables_col_1=( image_revised_last[:,:,0]==pixel_table )*1 - img_tables_col_1=img_tables_col_1.astype(np.uint8) - contours_table_col1,_=return_contours_of_image(img_tables_col_1) + img_tables_col_1 = (image_revised_last[:,:,0] == pixel_table).astype(np.uint8) + contours_table_col1, _ = return_contours_of_image(img_tables_col_1) _,_ ,_ , _, y_min_tab_col1 ,y_max_tab_col1, _= find_new_features_of_contours(contours_table_col1) @@ -2779,17 +2728,13 @@ class Eynollah: def get_tables_from_model(self, img, num_col_classifier): img_org = np.copy(img) - img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - - if not self.dir_in: self.model_table, _ = self.start_new_session_and_model(self.model_table_dir) patches = False - if self.light_version: prediction_table = self.do_prediction_new_concept(patches, img, self.model_table) prediction_table = prediction_table.astype(np.int16) @@ -2804,52 +2749,52 @@ class Eynollah: prediction_table = prediction_table.astype(np.int16) elif num_col_classifier ==2: - height_ext = 0#int( img.shape[0]/4. ) - h_start = int(height_ext/2.) - width_ext = int( img.shape[1]/8. ) - w_start = int(width_ext/2.) + height_ext = 0 # img.shape[0] // 4 + h_start = height_ext // 2 + width_ext = img.shape[1] // 8 + w_start = width_ext // 2 - height_new = img.shape[0]+height_ext - width_new = img.shape[1]+width_ext - - img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 - img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] + img_new = np.zeros((img.shape[0] + height_ext, + img.shape[1] + width_ext, + img.shape[2])).astype(float) + ys = slice(h_start, h_start + img.shape[0]) + xs = slice(w_start, w_start + img.shape[1]) + img_new[ys, xs] = img prediction_ext = self.do_prediction(patches, img_new, self.model_table) pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), self.model_table) pre_updown = cv2.flip(pre_updown, -1) - prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table = prediction_ext[ys, xs] + prediction_table_updown = pre_updown[ys, xs] prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 prediction_table = prediction_table.astype(np.int16) - elif num_col_classifier ==1: - height_ext = 0# int( img.shape[0]/4. ) - h_start = int(height_ext/2.) - width_ext = int( img.shape[1]/4. ) - w_start = int(width_ext/2.) + height_ext = 0 # img.shape[0] // 4 + h_start = height_ext // 2 + width_ext = img.shape[1] // 4 + w_start = width_ext // 2 - height_new = img.shape[0]+height_ext - width_new = img.shape[1]+width_ext - - img_new =np.ones((height_new,width_new,img.shape[2])).astype(float)*0 - img_new[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] =img[:,:,:] + img_new =np.zeros((img.shape[0] + height_ext, + img.shape[1] + width_ext, + img.shape[2])).astype(float) + ys = slice(h_start, h_start + img.shape[0]) + xs = slice(w_start, w_start + img.shape[1]) + img_new[ys, xs] = img prediction_ext = self.do_prediction(patches, img_new, self.model_table) pre_updown = self.do_prediction(patches, cv2.flip(img_new[:,:,:], -1), self.model_table) pre_updown = cv2.flip(pre_updown, -1) - prediction_table = prediction_ext[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] - prediction_table_updown = pre_updown[h_start:h_start+img.shape[0] ,w_start: w_start+img.shape[1], : ] + prediction_table = prediction_ext[ys, xs] + prediction_table_updown = pre_updown[ys, xs] prediction_table[:,:,0][prediction_table_updown[:,:,0]==1]=1 prediction_table = prediction_table.astype(np.int16) - else: prediction_table = np.zeros(img.shape) - img_w_half = int(img.shape[1]/2.) + img_w_half = img.shape[1] // 2 pre1 = self.do_prediction(patches, img[:,0:img_w_half,:], self.model_table) pre2 = self.do_prediction(patches, img[:,img_w_half:,:], self.model_table) @@ -2877,7 +2822,10 @@ class Eynollah: prediction_table_erode = cv2.dilate(prediction_table_erode, KERNEL, iterations=20) return prediction_table_erode.astype(np.int16) - def run_graphics_and_columns_light(self, text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light): + def run_graphics_and_columns_light( + self, text_regions_p_1, textline_mask_tot_ea, + num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light): + #print(text_regions_p_1.shape, 'text_regions_p_1 shape run graphics') #print(erosion_hurts, 'erosion_hurts') t_in_gr = time.time() @@ -2894,14 +2842,13 @@ class Eynollah: if self.tables: table_prediction = self.get_tables_from_model(image_page, num_col_classifier) else: - table_prediction = (np.zeros((image_page.shape[0], image_page.shape[1]))).astype(np.int16) + table_prediction = np.zeros((image_page.shape[0], image_page.shape[1])).astype(np.int16) if self.plotter: self.plotter.save_page_image(image_page) text_regions_p_1 = text_regions_p_1[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] textline_mask_tot_ea = textline_mask_tot_ea[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] - img_bin_light = img_bin_light[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] mask_images = (text_regions_p_1[:, :] == 2) * 1 @@ -2931,10 +2878,10 @@ class Eynollah: self.logger.error(why) num_col = None #print("inside graphics 3 ", time.time() - t_in_gr) - return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light + return (num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, + text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light) def run_graphics_and_columns_without_layout(self, textline_mask_tot_ea, img_bin_light): - #print(text_regions_p_1.shape, 'text_regions_p_1 shape run graphics') #print(erosion_hurts, 'erosion_hurts') t_in_gr = time.time() @@ -2950,11 +2897,14 @@ class Eynollah: #print("inside graphics 1 ", time.time() - t_in_gr) textline_mask_tot_ea = textline_mask_tot_ea[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] - img_bin_light = img_bin_light[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3]] return page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page - def run_graphics_and_columns(self, text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts): + + def run_graphics_and_columns( + self, text_regions_p_1, + num_col_classifier, num_column_is_classified, erosion_hurts): + t_in_gr = time.time() img_g = self.imread(grayscale=True, uint8=True) @@ -2969,7 +2919,7 @@ class Eynollah: if self.tables: table_prediction = self.get_tables_from_model(image_page, num_col_classifier) else: - table_prediction = (np.zeros((image_page.shape[0], image_page.shape[1]))).astype(np.int16) + table_prediction = np.zeros((image_page.shape[0], image_page.shape[1])).astype(np.int16) if self.plotter: self.plotter.save_page_image(image_page) @@ -2987,7 +2937,6 @@ class Eynollah: img_only_regions = np.copy(img_only_regions_with_sep[:,:]) else: img_only_regions = cv2.erode(img_only_regions_with_sep[:,:], KERNEL, iterations=6) - try: num_col, _ = find_num_col(img_only_regions, num_col_classifier, self.tables, multiplier=6.0) num_col = num_col + 1 @@ -2996,12 +2945,14 @@ class Eynollah: except Exception as why: self.logger.error(why) num_col = None - return num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction + return (num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, + text_regions_p_1, cont_page, table_prediction) - def run_enhancement(self,light_version): + def run_enhancement(self, light_version): t_in = time.time() self.logger.info("Resizing and enhancing image...") - is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = self.resize_and_enhance_image_with_column_classifier(light_version) + is_image_enhanced, img_org, img_res, num_col_classifier, num_column_is_classified, img_bin = \ + self.resize_and_enhance_image_with_column_classifier(light_version) self.logger.info("Image was %senhanced.", '' if is_image_enhanced else 'not ') scale = 1 if is_image_enhanced: @@ -3046,7 +2997,10 @@ class Eynollah: self.logger.info("slope_deskew: %.2f°", slope_deskew) return slope_deskew, slope_first - def run_marginals(self, image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction): + def run_marginals( + self, image_page, textline_mask_tot_ea, mask_images, mask_lines, + num_col_classifier, slope_deskew, text_regions_p_1, table_prediction): + image_page_rotated, textline_mask_tot = image_page[:, :], textline_mask_tot_ea[:, :] textline_mask_tot[mask_images[:, :] == 1] = 0 @@ -3060,7 +3014,9 @@ class Eynollah: if self.tables: regions_without_separators[table_prediction==1] = 1 regions_without_separators = regions_without_separators.astype(np.uint8) - text_regions_p = get_marginals(rotate_image(regions_without_separators, slope_deskew), text_regions_p, num_col_classifier, slope_deskew, light_version=self.light_version, kernel=KERNEL) + text_regions_p = get_marginals( + rotate_image(regions_without_separators, slope_deskew), text_regions_p, + num_col_classifier, slope_deskew, light_version=self.light_version, kernel=KERNEL) except Exception as e: self.logger.error("exception %s", e) @@ -3069,11 +3025,15 @@ class Eynollah: self.plotter.save_plot_of_layout_main(text_regions_p, image_page) return textline_mask_tot, text_regions_p, image_page_rotated - def run_boxes_no_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts): + def run_boxes_no_full_layout( + self, image_page, textline_mask_tot, text_regions_p, + slope_deskew, num_col_classifier, table_prediction, erosion_hurts): + self.logger.debug('enter run_boxes_no_full_layout') t_0_box = time.time() if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + _, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = rotation_not_90_func( + image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) table_prediction_n = resize_image(table_prediction_n, text_regions_p.shape[0], text_regions_p.shape[1]) @@ -3090,10 +3050,14 @@ class Eynollah: regions_without_separators_d = None pixel_lines = 3 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - _, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + _, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_lines) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_lines) #print(time.time()-t_0_box,'time box in 2') self.logger.info("num_col_classifier: %s", num_col_classifier) @@ -3107,7 +3071,9 @@ class Eynollah: #print(time.time()-t_0_box,'time box in 3') t1 = time.time() if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new, regions_without_separators, matrix_of_lines_ch, + num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes_d = None self.logger.debug("len(boxes): %s", len(boxes)) #print(time.time()-t_0_box,'time box in 3.1') @@ -3119,12 +3085,17 @@ class Eynollah: text_regions_p_tables = np.copy(text_regions_p) text_regions_p_tables[:,:][(table_prediction[:,:] == 1)] = 10 pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) + img_revised_tab2 = self.add_tables_heuristic_to_layout( + text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables, + num_col_classifier , 0.000005, pixel_line) #print(time.time()-t_0_box,'time box in 3.2') - img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction, 10, num_col_classifier) + img_revised_tab2, contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables( + img_revised_tab2, table_prediction, 10, num_col_classifier) #print(time.time()-t_0_box,'time box in 3.3') else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, + num_col_classifier, erosion_hurts, self.tables, self.right2left) boxes = None self.logger.debug("len(boxes): %s", len(boxes_d)) @@ -3137,8 +3108,11 @@ class Eynollah: text_regions_p_tables[:,:][(text_regions_p_tables[:,:] != 3) & (table_prediction_n[:,:] == 1)] = 10 pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) - img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2,table_prediction_n, 10, num_col_classifier) + img_revised_tab2 = self.add_tables_heuristic_to_layout( + text_regions_p_tables, boxes_d, 0, splitter_y_new_d, peaks_neg_tot_tables_d, text_regions_p_tables, + num_col_classifier, 0.000005, pixel_line) + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables( + img_revised_tab2, table_prediction_n, 10, num_col_classifier) img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) @@ -3185,55 +3159,71 @@ class Eynollah: contours_tables = return_contours_of_interested_region(text_regions_p, pixel_img, min_area_mar) #print(time.time()-t_0_box,'time box in 5') self.logger.debug('exit run_boxes_no_full_layout') - return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables + return (polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, + regions_without_separators_d, boxes, boxes_d, + polygons_of_marginals, contours_tables) + + def run_boxes_full_layout( + self, image_page, textline_mask_tot, text_regions_p, + slope_deskew, num_col_classifier, img_only_regions, + table_prediction, erosion_hurts, img_bin_light): - def run_boxes_full_layout(self, image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light): self.logger.debug('enter run_boxes_full_layout') t_full0 = time.time() if self.tables: if self.light_version: text_regions_p[:,:][table_prediction[:,:]==1] = 10 - img_revised_tab=text_regions_p[:,:] + img_revised_tab = text_regions_p[:,:] if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + image_page_rotated_n, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = \ + rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) - regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_separators_d = (text_regions_p_1_n[:,:] == 1)*1 regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 else: text_regions_p_1_n = None textline_mask_tot_d = None regions_without_separators_d = None - regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + # regions_without_separators = ( text_regions_p[:,:]==1 | text_regions_p[:,:]==2 )*1 + #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators = (text_regions_p[:,:] == 1)*1 regions_without_separators[table_prediction == 1] = 1 else: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - image_page_rotated_n,textline_mask_tot_d,text_regions_p_1_n , table_prediction_n = rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) + image_page_rotated_n, textline_mask_tot_d, text_regions_p_1_n, table_prediction_n = \ + rotation_not_90_func(image_page, textline_mask_tot, text_regions_p, table_prediction, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n,text_regions_p.shape[0],text_regions_p.shape[1]) textline_mask_tot_d = resize_image(textline_mask_tot_d,text_regions_p.shape[0],text_regions_p.shape[1]) table_prediction_n = resize_image(table_prediction_n,text_regions_p.shape[0],text_regions_p.shape[1]) - regions_without_separators_d=(text_regions_p_1_n[:,:] == 1)*1 + regions_without_separators_d = (text_regions_p_1_n[:,:] == 1)*1 regions_without_separators_d[table_prediction_n[:,:] == 1] = 1 else: text_regions_p_1_n = None textline_mask_tot_d = None regions_without_separators_d = None - - regions_without_separators = (text_regions_p[:,:] == 1)*1#( (text_regions_p[:,:]==1) | (text_regions_p[:,:]==2) )*1 #self.return_regions_without_seperators_new(text_regions_p[:,:,0],img_only_regions) + + # regions_without_separators = ( text_regions_p[:,:]==1 | text_regions_p[:,:]==2 )*1 + #self.return_regions_without_separators_new(text_regions_p[:,:,0],img_only_regions) + regions_without_separators = (text_regions_p[:,:] == 1)*1 regions_without_separators[table_prediction == 1] = 1 pixel_lines=3 if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, peaks_neg_fin, matrix_of_lines_ch, splitter_y_new, seperators_closeup_n = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_lines) if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - num_col_d, peaks_neg_fin_d, matrix_of_lines_ch_d, splitter_y_new_d, seperators_closeup_n_d = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2),num_col_classifier, self.tables, pixel_lines) + num_col_d, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_lines) if num_col_classifier>=3: if np.abs(slope_deskew) < SLOPE_THRESHOLD: @@ -3247,33 +3237,40 @@ class Eynollah: pass if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new, regions_without_separators, matrix_of_lines_ch, + num_col_classifier, erosion_hurts, self.tables, self.right2left) text_regions_p_tables = np.copy(text_regions_p) text_regions_p_tables[:,:][(table_prediction[:,:]==1)] = 10 pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables , num_col_classifier , 0.000005, pixel_line) - - img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction, 10, num_col_classifier) + img_revised_tab2 = self.add_tables_heuristic_to_layout( + text_regions_p_tables, boxes, 0, splitter_y_new, peaks_neg_tot_tables, text_regions_p_tables, + num_col_classifier , 0.000005, pixel_line) + img_revised_tab2,contoures_tables = self.check_iou_of_bounding_box_and_contour_for_tables( + img_revised_tab2, table_prediction, 10, num_col_classifier) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, + num_col_classifier, erosion_hurts, self.tables, self.right2left) text_regions_p_tables = np.copy(text_regions_p_1_n) text_regions_p_tables = np.round(text_regions_p_tables) text_regions_p_tables[:,:][(text_regions_p_tables[:,:]!=3) & (table_prediction_n[:,:]==1)] = 10 pixel_line = 3 - img_revised_tab2 = self.add_tables_heuristic_to_layout(text_regions_p_tables,boxes_d,0,splitter_y_new_d,peaks_neg_tot_tables_d,text_regions_p_tables, num_col_classifier, 0.000005, pixel_line) + img_revised_tab2 = self.add_tables_heuristic_to_layout( + text_regions_p_tables, boxes_d, 0, splitter_y_new_d, peaks_neg_tot_tables_d, text_regions_p_tables, + num_col_classifier, 0.000005, pixel_line) - img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables(img_revised_tab2, table_prediction_n, 10, num_col_classifier) + img_revised_tab2_d,_ = self.check_iou_of_bounding_box_and_contour_for_tables( + img_revised_tab2, table_prediction_n, 10, num_col_classifier) img_revised_tab2_d_rotated = rotate_image(img_revised_tab2_d, -slope_deskew) - img_revised_tab2_d_rotated = np.round(img_revised_tab2_d_rotated) img_revised_tab2_d_rotated = img_revised_tab2_d_rotated.astype(np.int8) img_revised_tab2_d_rotated = resize_image(img_revised_tab2_d_rotated, text_regions_p.shape[0], text_regions_p.shape[1]) - if np.abs(slope_deskew) < 0.13: img_revised_tab = np.copy(img_revised_tab2[:,:,0]) else: @@ -3281,7 +3278,6 @@ class Eynollah: img_revised_tab[:,:][img_revised_tab[:,:] == 10] = 0 img_revised_tab[:,:][img_revised_tab2_d_rotated[:,:,0] == 10] = 10 - ##img_revised_tab=img_revised_tab2[:,:,0] #img_revised_tab=text_regions_p[:,:] text_regions_p[:,:][text_regions_p[:,:]==10] = 0 @@ -3310,10 +3306,9 @@ class Eynollah: image_page = image_page.astype(np.uint8) #print("full inside 1", time.time()- t_full0) - if self.light_version: - regions_fully, regions_fully_only_drop = self.extract_text_regions_new(img_bin_light, False, cols=num_col_classifier) - else: - regions_fully, regions_fully_only_drop = self.extract_text_regions_new(image_page, False, cols=num_col_classifier) + regions_fully, regions_fully_only_drop = self.extract_text_regions_new( + img_bin_light if self.light_version else image_page, + False, cols=num_col_classifier) #print("full inside 2", time.time()- t_full0) # 6 is the separators lable in old full layout model # 4 is the drop capital class in old full layout model @@ -3328,7 +3323,6 @@ class Eynollah: drop_capital_label_in_full_layout_model = 3 drops = (regions_fully[:,:,0]==drop_capital_label_in_full_layout_model)*1 - drops= drops.astype(np.uint8) regions_fully[:,:,0][regions_fully[:,:,0]==drop_capital_label_in_full_layout_model] = 1 @@ -3336,8 +3330,8 @@ class Eynollah: drops = cv2.erode(drops[:,:], KERNEL, iterations=1) regions_fully[:,:,0][drops[:,:]==1] = drop_capital_label_in_full_layout_model - - regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout(regions_fully, drop_capital_label_in_full_layout_model, text_regions_p) + regions_fully = putt_bb_of_drop_capitals_of_model_in_patches_in_layout( + regions_fully, drop_capital_label_in_full_layout_model, text_regions_p) ##regions_fully_np, _ = self.extract_text_regions(image_page, False, cols=num_col_classifier) ##if num_col_classifier > 2: ##regions_fully_np[:, :, 0][regions_fully_np[:, :, 0] == 4] = 0 @@ -3353,7 +3347,8 @@ class Eynollah: #plt.show() ####if not self.tables: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout(image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) + _, textline_mask_tot_d, text_regions_p_1_n, regions_fully_n = rotation_not_90_func_full_layout( + image_page, textline_mask_tot, text_regions_p, regions_fully, slope_deskew) text_regions_p_1_n = resize_image(text_regions_p_1_n, text_regions_p.shape[0], text_regions_p.shape[1]) textline_mask_tot_d = resize_image(textline_mask_tot_d, text_regions_p.shape[0], text_regions_p.shape[1]) @@ -3371,18 +3366,19 @@ class Eynollah: self.logger.debug('exit run_boxes_full_layout') #print("full inside 3", time.time()- t_full0) - return polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables + return (polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, + regions_without_separators_d, regions_fully, regions_without_separators, + polygons_of_marginals, contours_tables) def our_load_model(self, model_file): - try: model = load_model(model_file, compile=False) except: - model = load_model(model_file , compile=False,custom_objects = {"PatchEncoder": PatchEncoder, "Patches": Patches}) - + model = load_model(model_file, compile=False, custom_objects={ + "PatchEncoder": PatchEncoder, "Patches": Patches}) return model - def do_order_of_regions_with_model(self,contours_only_text_parent, contours_only_text_parent_h, text_regions_p): + def do_order_of_regions_with_model(self, contours_only_text_parent, contours_only_text_parent_h, text_regions_p): y_len = text_regions_p.shape[0] x_len = text_regions_p.shape[1] @@ -3394,10 +3390,11 @@ class Eynollah: img_header_and_sep = np.zeros((y_len,x_len), dtype='uint8') if contours_only_text_parent_h: - _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours(contours_only_text_parent_h) + _, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, _ = find_new_features_of_contours( + contours_only_text_parent_h) for j in range(len(cy_main)): - img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12,int(x_min_main[j]):int(x_max_main[j]) ] = 1 - + img_header_and_sep[int(y_max_main[j]):int(y_max_main[j])+12, + int(x_min_main[j]):int(x_max_main[j])] = 1 co_text_all = contours_only_text_parent + contours_only_text_parent_h else: co_text_all = contours_only_text_parent @@ -3480,7 +3477,7 @@ class Eynollah: region_ids = ['region_%04d' % i for i in range(len(co_text_all))] return ordered, region_ids - def return_start_and_end_of_common_text_of_textline_ocr(self,textline_image, ind_tot): + def return_start_and_end_of_common_text_of_textline_ocr(self, textline_image, ind_tot): width = np.shape(textline_image)[1] height = np.shape(textline_image)[0] common_window = int(0.2*width) @@ -3492,18 +3489,14 @@ class Eynollah: sum_smoothed = gaussian_filter1d(img_sum, 3) peaks_real, _ = find_peaks(sum_smoothed, height=0) - if len(peaks_real)>70: print(len(peaks_real), 'len(peaks_real)') peaks_real = peaks_real[(peaks_realwidth1)] arg_sort = np.argsort(sum_smoothed[peaks_real]) - arg_sort4 =arg_sort[::-1][:4] - peaks_sort_4 = peaks_real[arg_sort][::-1][:4] - argsort_sorted = np.argsort(peaks_sort_4) first_4_sorted = peaks_sort_4[argsort_sorted] @@ -3522,9 +3515,8 @@ class Eynollah: return peaks_final[0], peaks_final[1] else: pass - - - def return_start_and_end_of_common_text_of_textline_ocr_without_common_section(self,textline_image, ind_tot): + + def return_start_and_end_of_common_text_of_textline_ocr_without_common_section(self, textline_image, ind_tot): width = np.shape(textline_image)[1] height = np.shape(textline_image)[0] common_window = int(0.06*width) @@ -3536,14 +3528,12 @@ class Eynollah: sum_smoothed = gaussian_filter1d(img_sum, 3) peaks_real, _ = find_peaks(sum_smoothed, height=0) - if len(peaks_real)>70: #print(len(peaks_real), 'len(peaks_real)') peaks_real = peaks_real[(peaks_realwidth1)] arg_max = np.argmax(sum_smoothed[peaks_real]) - peaks_final = peaks_real[arg_max] #plt.figure(ind_tot) @@ -3555,15 +3545,15 @@ class Eynollah: return peaks_final else: return None - def return_start_and_end_of_common_text_of_textline_ocr_new_splitted(self,peaks_real, sum_smoothed, start_split, end_split): + + def return_start_and_end_of_common_text_of_textline_ocr_new_splitted( + self, peaks_real, sum_smoothed, start_split, end_split): + peaks_real = peaks_real[(peaks_realstart_split)] arg_sort = np.argsort(sum_smoothed[peaks_real]) - arg_sort4 =arg_sort[::-1][:4] - peaks_sort_4 = peaks_real[arg_sort][::-1][:4] - argsort_sorted = np.argsort(peaks_sort_4) first_4_sorted = peaks_sort_4[argsort_sorted] @@ -3573,8 +3563,8 @@ class Eynollah: arg_sortnew = np.argsort(y_4_sorted) peaks_final =np.sort( first_4_sorted[arg_sortnew][3:] ) return peaks_final[0] - - def return_start_and_end_of_common_text_of_textline_ocr_new(self,textline_image, ind_tot): + + def return_start_and_end_of_common_text_of_textline_ocr_new(self, textline_image, ind_tot): width = np.shape(textline_image)[1] height = np.shape(textline_image)[0] common_window = int(0.15*width) @@ -3587,11 +3577,11 @@ class Eynollah: sum_smoothed = gaussian_filter1d(img_sum, 3) peaks_real, _ = find_peaks(sum_smoothed, height=0) - if len(peaks_real)>70: - peak_start = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted(peaks_real, sum_smoothed, width1, mid+2) - - peak_end = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted(peaks_real, sum_smoothed, mid-2, width2) + peak_start = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted( + peaks_real, sum_smoothed, width1, mid+2) + peak_end = self.return_start_and_end_of_common_text_of_textline_ocr_new_splitted( + peaks_real, sum_smoothed, mid-2, width2) #plt.figure(ind_tot) #plt.imshow(textline_image) @@ -3602,23 +3592,23 @@ class Eynollah: return peak_start, peak_end else: pass - - def return_ocr_of_textline_without_common_section(self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + + def return_ocr_of_textline_without_common_section( + self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + if h2w_ratio > 0.05: pixel_values = processor(textline_image, return_tensors="pt").pixel_values generated_ids = model_ocr.generate(pixel_values.to(device)) generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] else: - #width = np.shape(textline_image)[1] #height = np.shape(textline_image)[0] #common_window = int(0.3*width) - #width1 = int ( width/2. - common_window ) #width2 = int ( width/2. + common_window ) - - split_point = self.return_start_and_end_of_common_text_of_textline_ocr_without_common_section(textline_image, ind_tot) + split_point = self.return_start_and_end_of_common_text_of_textline_ocr_without_common_section( + textline_image, ind_tot) if split_point: image1 = textline_image[:, :split_point,:]# image.crop((0, 0, width2, height)) image2 = textline_image[:, split_point:,:]#image.crop((width1, 0, width, height)) @@ -3652,7 +3642,10 @@ class Eynollah: #print(generated_text,'generated_text') #print('########################################') return generated_text - def return_ocr_of_textline(self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + + def return_ocr_of_textline( + self, textline_image, model_ocr, processor, device, width_textline, h2w_ratio,ind_tot): + if h2w_ratio > 0.05: pixel_values = processor(textline_image, return_tensors="pt").pixel_values generated_ids = model_ocr.generate(pixel_values.to(device)) @@ -3661,7 +3654,6 @@ class Eynollah: #width = np.shape(textline_image)[1] #height = np.shape(textline_image)[0] #common_window = int(0.3*width) - #width1 = int ( width/2. - common_window ) #width2 = int ( width/2. + common_window ) @@ -3683,8 +3675,8 @@ class Eynollah: #print(generated_text2, 'generated_text2') #print('########################################') - match = sq(None, generated_text1, generated_text2).find_longest_match(0, len(generated_text1), 0, len(generated_text2)) - + match = sq(None, generated_text1, generated_text2).find_longest_match( + 0, len(generated_text1), 0, len(generated_text2)) generated_text = generated_text1 + generated_text2[match.b+match.size:] except: pixel_values = processor(textline_image, return_tensors="pt").pixel_values @@ -3692,43 +3684,44 @@ class Eynollah: generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0] return generated_text - + def return_textline_contour_with_added_box_coordinate(self, textline_contour, box_ind): textline_contour[:,0] = textline_contour[:,0] + box_ind[2] textline_contour[:,1] = textline_contour[:,1] + box_ind[0] return textline_contour + def return_list_of_contours_with_desired_order(self, ls_cons, sorted_indexes): return [ls_cons[sorted_indexes[index]] for index in range(len(sorted_indexes))] - - def return_it_in_two_groups(self,x_differential): - split = [ind if x_differential[ind]!=x_differential[ind+1] else -1 for ind in range(len(x_differential)-1)] + def return_it_in_two_groups(self, x_differential): + split = [ind if x_differential[ind]!=x_differential[ind+1] else -1 + for ind in range(len(x_differential)-1)] split_masked = list( np.array(split[:])[np.array(split[:])!=-1] ) - if 0 not in split_masked: split_masked.insert(0, -1) - split_masked.append(len(x_differential)-1) split_masked = np.array(split_masked) +1 - sums = [np.sum(x_differential[split_masked[ind]:split_masked[ind+1]]) for ind in range(len(split_masked)-1)] - - indexes_to_bec_changed = [ind if ( np.abs(sums[ind-1]) > np.abs(sums[ind]) and np.abs(sums[ind+1]) > np.abs(sums[ind])) else -1 for ind in range(1,len(sums)-1) ] + sums = [np.sum(x_differential[split_masked[ind]:split_masked[ind+1]]) + for ind in range(len(split_masked)-1)] + indexes_to_bec_changed = [ind if (np.abs(sums[ind-1]) > np.abs(sums[ind]) and + np.abs(sums[ind+1]) > np.abs(sums[ind])) else -1 + for ind in range(1,len(sums)-1)] indexes_to_bec_changed_filtered = np.array(indexes_to_bec_changed)[np.array(indexes_to_bec_changed)!=-1] x_differential_new = np.copy(x_differential) for i in indexes_to_bec_changed_filtered: - x_differential_new[split_masked[i]:split_masked[i+1]] = -1*np.array(x_differential)[split_masked[i]:split_masked[i+1]] + i_slice = slice(split_masked[i], split_masked[i+1]) + x_differential_new[i_slice] = -1 * np.array(x_differential)[i_slice] return x_differential_new - def dilate_textregions_contours_textline_version(self,all_found_textline_polygons): + + def dilate_textregions_contours_textline_version(self, all_found_textline_polygons): #print(all_found_textline_polygons) - for j in range(len(all_found_textline_polygons)): for ij in range(len(all_found_textline_polygons[j])): - con_ind = all_found_textline_polygons[j][ij] area = cv2.contourArea(con_ind) con_ind = con_ind.astype(np.float) @@ -3736,7 +3729,6 @@ class Eynollah: x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) - x_differential = gaussian_filter1d(x_differential, 0.1) y_differential = gaussian_filter1d(y_differential, 0.1) @@ -3754,7 +3746,6 @@ class Eynollah: inc_x = np.zeros(len(x_differential)+1) inc_y = np.zeros(len(x_differential)+1) - if (y_max-y_min) <= (x_max-x_min): dilation_m1 = round(area / (x_max-x_min) * 0.12) else: @@ -3786,7 +3777,6 @@ class Eynollah: inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) - inc_x[0] = inc_x[-1] inc_y[0] = inc_y[-1] @@ -3802,20 +3792,16 @@ class Eynollah: con_ind = con_ind.astype(np.int32) - results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] - + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) + for ind in range(len(con_scaled[:,0, 1])) ] results = np.array(results) - #print(results,'results') - results[results==0] = 1 - diff_result = np.diff(results) indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] - if results[0]==1: con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] @@ -3823,27 +3809,22 @@ class Eynollah: #indices_2 = indices_2[1:] indices_m2 = indices_m2[1:] - - if len(indices_2)>len(indices_m2): con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] - indices_2 = indices_2[:-1] - for ii in range(len(indices_2)): con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] - all_found_textline_polygons[j][ij][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons - def dilate_textregions_contours(self,all_found_textline_polygons): + + def dilate_textregions_contours(self, all_found_textline_polygons): #print(all_found_textline_polygons) for j in range(len(all_found_textline_polygons)): - con_ind = all_found_textline_polygons[j] #print(len(con_ind[:,0,0]),'con_ind[:,0,0]') area = cv2.contourArea(con_ind) @@ -3852,7 +3833,6 @@ class Eynollah: x_differential = np.diff( con_ind[:,0,0]) y_differential = np.diff( con_ind[:,0,1]) - x_differential = gaussian_filter1d(x_differential, 0.1) y_differential = gaussian_filter1d(y_differential, 0.1) @@ -3870,7 +3850,6 @@ class Eynollah: inc_x = np.zeros(len(x_differential)+1) inc_y = np.zeros(len(x_differential)+1) - if (y_max-y_min) <= (x_max-x_min): dilation_m1 = round(area / (x_max-x_min) * 0.12) else: @@ -3902,7 +3881,6 @@ class Eynollah: inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) - inc_x[0] = inc_x[-1] inc_y[0] = inc_y[-1] @@ -3918,50 +3896,38 @@ class Eynollah: con_ind = con_ind.astype(np.int32) - results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] - + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) + for ind in range(len(con_scaled[:,0, 1])) ] results = np.array(results) - #print(results,'results') - results[results==0] = 1 - diff_result = np.diff(results) - indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] indices_m2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==-2] - if results[0]==1: con_scaled[:indices_m2[0]+1,0, 1] = con_ind[:indices_m2[0]+1,0,1] con_scaled[:indices_m2[0]+1,0, 0] = con_ind[:indices_m2[0]+1,0,0] #indices_2 = indices_2[1:] indices_m2 = indices_m2[1:] - - if len(indices_2)>len(indices_m2): con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] - indices_2 = indices_2[:-1] - for ii in range(len(indices_2)): con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 0] = con_scaled[indices_2[ii],0, 0] - all_found_textline_polygons[j][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons - - def dilate_textline_contours(self,all_found_textline_polygons): + def dilate_textline_contours(self, all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): for ij in range(len(all_found_textline_polygons[j])): - con_ind = all_found_textline_polygons[j][ij] area = cv2.contourArea(con_ind) @@ -3991,7 +3957,6 @@ class Eynollah: dilation_m1 = round(area / (x_max-x_min) * 0.35) else: dilation_m1 = round(area / (y_max-y_min) * 0.35) - if dilation_m1>12: dilation_m1 = 12 @@ -4017,7 +3982,6 @@ class Eynollah: else: inc_x[i+1] = dilation_m2*(-1*y_differential_mask_nonzeros[i]) inc_y[i+1] = dilation_m2*(x_differential_mask_nonzeros[i]) - inc_x[0] = inc_x[-1] inc_y[0] = inc_y[-1] @@ -4030,16 +3994,13 @@ class Eynollah: con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 - con_ind = con_ind.astype(np.int32) - results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) for ind in range(len(con_scaled[:,0, 1])) ] - + results = [cv2.pointPolygonTest(con_ind, (con_scaled[ind,0, 0], con_scaled[ind,0, 1]), False) + for ind in range(len(con_scaled[:,0, 1])) ] results = np.array(results) - results[results==0] = 1 - diff_result = np.diff(results) indices_2 = [ind for ind in range(len(diff_result)) if diff_result[ind]==2] @@ -4050,13 +4011,10 @@ class Eynollah: con_scaled[:indices_m2[0]+1,0, 0] = con_ind[:indices_m2[0]+1,0,0] indices_m2 = indices_m2[1:] - - if len(indices_2)>len(indices_m2): con_scaled[indices_2[-1]+1:,0, 1] = con_ind[indices_2[-1]+1:,0,1] con_scaled[indices_2[-1]+1:,0, 0] = con_ind[indices_2[-1]+1:,0,0] indices_2 = indices_2[:-1] - for ii in range(len(indices_2)): con_scaled[indices_2[ii]+1:indices_m2[ii]+1,0, 1] = con_scaled[indices_2[ii],0, 1] @@ -4071,12 +4029,11 @@ class Eynollah: areas = [cv2.contourArea(contours[j]) for j in range(len(contours))] area_tot = image.shape[0]*image.shape[1] - M_main = [cv2.moments(contours[j]) for j in range(len(contours))] + M_main = [cv2.moments(contours[j]) + for j in range(len(contours))] cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] - - areas_ratio = np.array(areas)/ area_tot contours_index_small = [ind for ind in range(len(contours)) if areas_ratio[ind] < 1e-3] contours_index_big = [ind for ind in range(len(contours)) if areas_ratio[ind] >= 1e-3] @@ -4084,9 +4041,11 @@ class Eynollah: #contours_> = [contours[ind] for ind in contours_index_big] indexes_to_be_removed = [] for ind_small in contours_index_small: - results = [cv2.pointPolygonTest(contours[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in contours_index_big ] + results = [cv2.pointPolygonTest(contours[ind], (cx_main[ind_small], cy_main[ind_small]), False) + for ind in contours_index_big] if marginal_cnts: - results_marginal = [cv2.pointPolygonTest(marginal_cnts[ind], (cx_main[ind_small], cy_main[ind_small]), False) for ind in range(len(marginal_cnts)) ] + results_marginal = [cv2.pointPolygonTest(marginal_cnts[ind], (cx_main[ind_small], cy_main[ind_small]), False) + for ind in range(len(marginal_cnts))] results_marginal = np.array(results_marginal) if np.any(results_marginal==1): @@ -4096,7 +4055,6 @@ class Eynollah: if np.any(results==1): indexes_to_be_removed.append(ind_small) - if len(indexes_to_be_removed)>0: indexes_to_be_removed = np.unique(indexes_to_be_removed) @@ -4105,8 +4063,7 @@ class Eynollah: contours.pop(ind) return contours - - + else: contours_txtline_of_all_textregions = [] indexes_of_textline_tot = [] @@ -4115,32 +4072,23 @@ class Eynollah: for jj in range(len(contours)): contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours[jj] - ind_ins = np.zeros( len(contours[jj]) ) + jj - list_ind_ins = list(ind_ins) + ind_textline_inside_tr = list(range(len(contours[jj]))) + index_textline_inside_textregion = index_textline_inside_textregion + ind_textline_inside_tr + ind_ins = [0] * len(contours[jj]) + jj + indexes_of_textline_tot = indexes_of_textline_tot + ind_ins - ind_textline_inside_tr = np.array (range(len(contours[jj])) ) - - list_ind_textline_inside_tr = list(ind_textline_inside_tr) - - index_textline_inside_textregion = index_textline_inside_textregion + list_ind_textline_inside_tr - - indexes_of_textline_tot = indexes_of_textline_tot + list_ind_ins - - - M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] + M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) + for j in range(len(contours_txtline_of_all_textregions))] cx_main_tot = [(M_main_tot[j]["m10"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] cy_main_tot = [(M_main_tot[j]["m01"] / (M_main_tot[j]["m00"] + 1e-32)) for j in range(len(M_main_tot))] - areas_tot = [cv2.contourArea(con_ind) for con_ind in contours_txtline_of_all_textregions] area_tot_tot = image.shape[0]*image.shape[1] textregion_index_to_del = [] textline_in_textregion_index_to_del = [] for ij in range(len(contours_txtline_of_all_textregions)): - args_all = list(np.array(range(len(contours_txtline_of_all_textregions)))) - args_all.pop(ij) areas_without = np.array(areas_tot)[args_all] @@ -4149,38 +4097,38 @@ class Eynollah: args_with_bigger_area = np.array(args_all)[areas_without > 1.5*area_of_con_interest] if len(args_with_bigger_area)>0: - results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main_tot[ij], cy_main_tot[ij]), False) for ind in args_with_bigger_area ] + results = [cv2.pointPolygonTest(contours_txtline_of_all_textregions[ind], (cx_main_tot[ij], cy_main_tot[ij]), False) + for ind in args_with_bigger_area ] results = np.array(results) if np.any(results==1): #print(indexes_of_textline_tot[ij], index_textline_inside_textregion[ij]) textregion_index_to_del.append(int(indexes_of_textline_tot[ij])) textline_in_textregion_index_to_del.append(int(index_textline_inside_textregion[ij])) #contours[int(indexes_of_textline_tot[ij])].pop(int(index_textline_inside_textregion[ij])) - - uniqe_args_trs = np.unique(textregion_index_to_del) - - for ind_u_a_trs in uniqe_args_trs: - textline_in_textregion_index_to_del_ind = np.array(textline_in_textregion_index_to_del)[np.array(textregion_index_to_del)==ind_u_a_trs] + + textregion_index_to_del = np.array(textregion_index_to_del) + textline_in_textregion_index_to_del = np.array(textline_in_textregion_index_to_del) + for ind_u_a_trs in np.unique(textregion_index_to_del): + textline_in_textregion_index_to_del_ind = textline_in_textregion_index_to_del[textregion_index_to_del==ind_u_a_trs] textline_in_textregion_index_to_del_ind = np.sort(textline_in_textregion_index_to_del_ind)[::-1] - for ittrd in textline_in_textregion_index_to_del_ind: contours[ind_u_a_trs].pop(ittrd) return contours - - - def filter_contours_without_textline_inside(self,contours,text_con_org, contours_textline, contours_only_text_parent_d_ordered): - + def filter_contours_without_textline_inside( + self, contours,text_con_org, contours_textline, contours_only_text_parent_d_ordered): + ###contours_txtline_of_all_textregions = [] - ###for jj in range(len(contours_textline)): ###contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours_textline[jj] - ###M_main_textline = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] - ###cx_main_textline = [(M_main_textline[j]["m10"] / (M_main_textline[j]["m00"] + 1e-32)) for j in range(len(M_main_textline))] - ###cy_main_textline = [(M_main_textline[j]["m01"] / (M_main_textline[j]["m00"] + 1e-32)) for j in range(len(M_main_textline))] - + ###M_main_textline = [cv2.moments(contours_txtline_of_all_textregions[j]) + ### for j in range(len(contours_txtline_of_all_textregions))] + ###cx_main_textline = [(M_main_textline[j]["m10"] / (M_main_textline[j]["m00"] + 1e-32)) + ### for j in range(len(M_main_textline))] + ###cy_main_textline = [(M_main_textline[j]["m01"] / (M_main_textline[j]["m00"] + 1e-32)) + ### for j in range(len(M_main_textline))] ###M_main = [cv2.moments(contours[j]) for j in range(len(contours))] ###cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] @@ -4188,8 +4136,8 @@ class Eynollah: ###contours_with_textline = [] ###for ind_tr, con_tr in enumerate(contours): - ###results = [cv2.pointPolygonTest(con_tr, (cx_main_textline[index_textline_con], cy_main_textline[index_textline_con]), False) for index_textline_con in range(len(contours_txtline_of_all_textregions)) ] - + ###results = [cv2.pointPolygonTest(con_tr, (cx_main_textline[index_textline_con], cy_main_textline[index_textline_con]), False) + ### for index_textline_con in range(len(contours_txtline_of_all_textregions)) ] ###results = np.array(results) ###if np.any(results==1): ###contours_with_textline.append(con_tr) @@ -4202,7 +4150,6 @@ class Eynollah: uniqe_args_trs = np.unique(textregion_index_to_del) uniqe_args_trs_sorted = np.sort(uniqe_args_trs)[::-1] - for ind_u_a_trs in uniqe_args_trs_sorted: contours.pop(ind_u_a_trs) contours_textline.pop(ind_u_a_trs) @@ -4211,11 +4158,10 @@ class Eynollah: return contours, text_con_org, contours_textline, contours_only_text_parent_d_ordered, np.array(range(len(contours))) - def dilate_textlines(self,all_found_textline_polygons): + def dilate_textlines(self, all_found_textline_polygons): for j in range(len(all_found_textline_polygons)): for i in range(len(all_found_textline_polygons[j])): con_ind = all_found_textline_polygons[j][i] - con_ind = con_ind.astype(np.float) x_differential = np.diff( con_ind[:,0,0]) @@ -4227,11 +4173,8 @@ class Eynollah: x_max = float(np.max( con_ind[:,0,0] )) y_max = float(np.max( con_ind[:,0,1] )) - if (y_max - y_min) > (x_max - x_min) and (x_max - x_min)<70: - x_biger_than_x = np.abs(x_differential) > np.abs(y_differential) - mult = x_biger_than_x*x_differential arg_min_mult = np.argmin(mult) @@ -4239,33 +4182,25 @@ class Eynollah: if y_differential[0]==0: y_differential[0] = 0.1 - if y_differential[-1]==0: y_differential[-1]= 0.1 - - - - y_differential = [y_differential[ind] if y_differential[ind]!=0 else (y_differential[ind-1] + y_differential[ind+1])/2. for ind in range(len(y_differential)) ] - + y_differential = [y_differential[ind] if y_differential[ind] != 0 + else 0.5 * (y_differential[ind-1] + y_differential[ind+1]) + for ind in range(len(y_differential))] if y_differential[0]==0.1: y_differential[0] = y_differential[1] if y_differential[-1]==0.1: y_differential[-1] = y_differential[-2] - y_differential.append(y_differential[0]) - y_differential = [-1 if y_differential[ind]<0 else 1 for ind in range(len(y_differential))] - + y_differential = [-1 if y_differential[ind] < 0 else 1 + for ind in range(len(y_differential))] y_differential = self.return_it_in_two_groups(y_differential) - y_differential = np.array(y_differential) - con_scaled = con_ind*1 - con_scaled[:,0, 0] = con_ind[:,0,0] - 8*y_differential - con_scaled[arg_min_mult,0, 1] = con_ind[arg_min_mult,0,1] + 8 con_scaled[arg_min_mult+1,0, 1] = con_ind[arg_min_mult+1,0,1] + 8 @@ -4284,10 +4219,8 @@ class Eynollah: except: pass - else: y_biger_than_x = np.abs(y_differential) > np.abs(x_differential) - mult = y_biger_than_x*y_differential arg_min_mult = np.argmin(mult) @@ -4295,32 +4228,25 @@ class Eynollah: if x_differential[0]==0: x_differential[0] = 0.1 - if x_differential[-1]==0: x_differential[-1]= 0.1 - - - - x_differential = [x_differential[ind] if x_differential[ind]!=0 else (x_differential[ind-1] + x_differential[ind+1])/2. for ind in range(len(x_differential)) ] - + x_differential = [x_differential[ind] if x_differential[ind] != 0 + else 0.5 * (x_differential[ind-1] + x_differential[ind+1]) + for ind in range(len(x_differential))] if x_differential[0]==0.1: x_differential[0] = x_differential[1] if x_differential[-1]==0.1: x_differential[-1] = x_differential[-2] - x_differential.append(x_differential[0]) - x_differential = [-1 if x_differential[ind]<0 else 1 for ind in range(len(x_differential))] - + x_differential = [-1 if x_differential[ind] < 0 else 1 + for ind in range(len(x_differential))] x_differential = self.return_it_in_two_groups(x_differential) x_differential = np.array(x_differential) - con_scaled = con_ind*1 - con_scaled[:,0, 1] = con_ind[:,0,1] + 8*x_differential - con_scaled[arg_min_mult,0, 0] = con_ind[arg_min_mult,0,0] + 8 con_scaled[arg_min_mult+1,0, 0] = con_ind[arg_min_mult+1,0,0] + 8 @@ -4338,17 +4264,19 @@ class Eynollah: con_scaled[arg_max_mult+2,0, 0] = con_ind[arg_max_mult+2,0,0] - 5 except: pass - - + con_scaled[:,0, 1][con_scaled[:,0, 1]<0] = 0 con_scaled[:,0, 0][con_scaled[:,0, 0]<0] = 0 all_found_textline_polygons[j][i][:,0,1] = con_scaled[:,0, 1] all_found_textline_polygons[j][i][:,0,0] = con_scaled[:,0, 0] - + return all_found_textline_polygons - def delete_regions_without_textlines(self,slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con): + def delete_regions_without_textlines( + self, slopes, all_found_textline_polygons, boxes_text, txt_con_org, + contours_only_text_parent, index_by_text_par_con): + slopes_rem = [] all_found_textline_polygons_rem = [] boxes_text_rem = [] @@ -4368,9 +4296,11 @@ class Eynollah: index_sort = np.argsort(index_by_text_par_con_rem) indexes_new = np.array(range(len(index_by_text_par_con_rem))) - index_by_text_par_con_rem_sort = [indexes_new[index_sort==j][0] for j in range(len(index_by_text_par_con_rem))] + index_by_text_par_con_rem_sort = [indexes_new[index_sort==j][0] + for j in range(len(index_by_text_par_con_rem))] - return slopes_rem, all_found_textline_polygons_rem, boxes_text_rem, txt_con_org_rem, contours_only_text_parent_rem, index_by_text_par_con_rem_sort + return (slopes_rem, all_found_textline_polygons_rem, boxes_text_rem, txt_con_org_rem, + contours_only_text_parent_rem, index_by_text_par_con_rem_sort) def run(self): """ @@ -4400,10 +4330,13 @@ class Eynollah: img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) if self.extract_only_images: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml,polygons_of_images,image_page, page_coord, cont_page = self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) + text_regions_p_1, erosion_hurts, polygons_lines_xml, polygons_of_images, image_page, page_coord, cont_page = \ + self.get_regions_light_v_extract_only_images(img_res, is_image_enhanced, num_col_classifier) ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - + pcgts = self.writer.build_pagexml_no_full_layout( + [], page_coord, [], [], [], [], + polygons_of_images, [], [], [], [], [], + cont_page, [], [], ocr_all_textlines) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) @@ -4414,21 +4347,26 @@ class Eynollah: return pcgts if self.skip_layout_and_reading_order: - _ ,_, _, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, - skip_layout_and_reading_order=self.skip_layout_and_reading_order) + _ ,_, _, textline_mask_tot_ea, img_bin_light = \ + self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier, + skip_layout_and_reading_order=self.skip_layout_and_reading_order) - page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) + page_coord, image_page, textline_mask_tot_ea, img_bin_light, cont_page = \ + self.run_graphics_and_columns_without_layout(textline_mask_tot_ea, img_bin_light) ##all_found_textline_polygons =self.scale_contours_new(textline_mask_tot_ea) cnt_clean_rot_raw, hir_on_cnt_clean_rot = return_contours_of_image(textline_mask_tot_ea) - all_found_textline_polygons = filter_contours_area_of_image(textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) + all_found_textline_polygons = filter_contours_area_of_image( + textline_mask_tot_ea, cnt_clean_rot_raw, hir_on_cnt_clean_rot, max_area=1, min_area=0.00001) all_found_textline_polygons=[ all_found_textline_polygons ] - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") + all_found_textline_polygons = self.dilate_textregions_contours_textline_version( + all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one( + all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") order_text_new = [0] @@ -4443,10 +4381,11 @@ class Eynollah: polygons_lines_xml = [] contours_tables = [] ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout(cont_page, page_coord, order_text_new, id_of_texts_tot, - all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, - cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + pcgts = self.writer.build_pagexml_no_full_layout( + cont_page, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) if self.dir_in: self.writer.write_pagexml(pcgts) continue @@ -4456,17 +4395,16 @@ class Eynollah: #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() if self.light_version: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) + text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = \ + self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) #print("text region early -2 in %.1fs", time.time() - t0) if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: img_w_new = 1000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: + else: img_w_new = 1300 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + img_h_new = img_w_new * textline_mask_tot_ea.shape[0] // textline_mask_tot_ea.shape[1] textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) @@ -4475,18 +4413,23 @@ class Eynollah: slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ - self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, \ + text_regions_p_1, cont_page, table_prediction, textline_mask_tot_ea, img_bin_light = \ + self.run_graphics_and_columns_light(text_regions_p_1, textline_mask_tot_ea, + num_col_classifier, num_column_is_classified, erosion_hurts, img_bin_light) #self.logger.info("run graphics %.1fs ", time.time() - t1t) #print("text region early -3 in %.1fs", time.time() - t0) textline_mask_tot_ea_org = np.copy(textline_mask_tot_ea) #print("text region early -4 in %.1fs", time.time() - t0) else: - text_regions_p_1 ,erosion_hurts, polygons_lines_xml = self.get_regions_from_xy_2models(img_res, is_image_enhanced, num_col_classifier) + text_regions_p_1 ,erosion_hurts, polygons_lines_xml = \ + self.get_regions_from_xy_2models(img_res, is_image_enhanced, + num_col_classifier) self.logger.info("Textregion detection took %.1fs ", time.time() - t1) t1 = time.time() - num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, text_regions_p_1, cont_page, table_prediction = \ + num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, \ + text_regions_p_1, cont_page, table_prediction = \ self.run_graphics_and_columns(text_regions_p_1, num_col_classifier, num_column_is_classified, erosion_hurts) self.logger.info("Graphics detection took %.1fs ", time.time() - t1) #self.logger.info('cont_page %s', cont_page) @@ -4496,7 +4439,9 @@ class Eynollah: if not num_col: self.logger.info("No columns detected, outputting an empty PAGE-XML") ocr_all_textlines = None - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) + pcgts = self.writer.build_pagexml_no_full_layout( + [], page_coord, [], [], [], [], [], [], [], [], [], [], + cont_page, [], [], ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t1) if self.dir_in: self.writer.write_pagexml(pcgts) @@ -4517,11 +4462,9 @@ class Eynollah: org_w_l_m = textline_mask_tot_ea.shape[1] if num_col_classifier == 1: img_w_new = 2000 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) - - elif num_col_classifier == 2: + else: img_w_new = 2400 - img_h_new = int(textline_mask_tot_ea.shape[0] / float(textline_mask_tot_ea.shape[1]) * img_w_new) + img_h_new = img_w_new * textline_mask_tot_ea.shape[0] // textline_mask_tot_ea.shape[1] image_page = resize_image(image_page,img_h_new, img_w_new ) textline_mask_tot_ea = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) @@ -4530,7 +4473,9 @@ class Eynollah: text_regions_p_1 = resize_image(text_regions_p_1,img_h_new, img_w_new ) table_prediction = resize_image(table_prediction,img_h_new, img_w_new ) - textline_mask_tot, text_regions_p, image_page_rotated = self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) + textline_mask_tot, text_regions_p, image_page_rotated = \ + self.run_marginals(image_page, textline_mask_tot_ea, mask_images, mask_lines, + num_col_classifier, slope_deskew, text_regions_p_1, table_prediction) if self.light_version and num_col_classifier in (1,2): image_page = resize_image(image_page,org_h_l_m, org_w_l_m ) @@ -4546,12 +4491,17 @@ class Eynollah: ## birdan sora chock chakir t1 = time.time() if not self.full_layout: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, boxes, boxes_d, polygons_of_marginals, contours_tables = \ - self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, table_prediction, erosion_hurts) + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, \ + boxes, boxes_d, polygons_of_marginals, contours_tables = \ + self.run_boxes_no_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, + num_col_classifier, table_prediction, erosion_hurts) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) else: - polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ - self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, num_col_classifier, img_only_regions, table_prediction, erosion_hurts, img_bin_light if self.light_version else None) + polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, \ + regions_fully, regions_without_separators, polygons_of_marginals, contours_tables = \ + self.run_boxes_full_layout(image_page, textline_mask_tot, text_regions_p, slope_deskew, + num_col_classifier, img_only_regions, table_prediction, erosion_hurts, + img_bin_light if self.light_version else None) ###polygons_of_marginals = self.dilate_textregions_contours(polygons_of_marginals) if self.light_version: drop_label_in_full_layout = 4 @@ -4572,18 +4522,23 @@ class Eynollah: areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) #self.logger.info('areas_cnt_text %s', areas_cnt_text) contours_biggest = contours_only_text_parent[np.argmax(areas_cnt_text)] - contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) if areas_cnt_text[jz] > MIN_AREA_REGION] + contours_only_text_parent = [c for jz, c in enumerate(contours_only_text_parent) + if areas_cnt_text[jz] > MIN_AREA_REGION] areas_cnt_text_parent = [area for area in areas_cnt_text if area > MIN_AREA_REGION] index_con_parents = np.argsort(areas_cnt_text_parent) - contours_only_text_parent = self.return_list_of_contours_with_desired_order(contours_only_text_parent, index_con_parents) + contours_only_text_parent = self.return_list_of_contours_with_desired_order( + contours_only_text_parent, index_con_parents) ##try: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) + ##contours_only_text_parent = \ + ##list(np.array(contours_only_text_parent,dtype=object)[index_con_parents]) ##except: - ##contours_only_text_parent = list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) + ##contours_only_text_parent = \ + ##list(np.array(contours_only_text_parent,dtype=np.int32)[index_con_parents]) ##areas_cnt_text_parent = list(np.array(areas_cnt_text_parent)[index_con_parents]) - areas_cnt_text_parent = self.return_list_of_contours_with_desired_order(areas_cnt_text_parent, index_con_parents) + areas_cnt_text_parent = self.return_list_of_contours_with_desired_order( + areas_cnt_text_parent, index_con_parents) cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) @@ -4598,14 +4553,17 @@ class Eynollah: if len(areas_cnt_text_d)>0: contours_biggest_d = contours_only_text_parent_d[np.argmax(areas_cnt_text_d)] index_con_parents_d = np.argsort(areas_cnt_text_d) - contours_only_text_parent_d = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d, index_con_parents_d) + contours_only_text_parent_d = self.return_list_of_contours_with_desired_order( + contours_only_text_parent_d, index_con_parents_d) #try: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) + #contours_only_text_parent_d = \ + #list(np.array(contours_only_text_parent_d,dtype=object)[index_con_parents_d]) #except: - #contours_only_text_parent_d = list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) - + #contours_only_text_parent_d = \ + #list(np.array(contours_only_text_parent_d,dtype=np.int32)[index_con_parents_d]) #areas_cnt_text_d = list(np.array(areas_cnt_text_d)[index_con_parents_d]) - areas_cnt_text_d = self.return_list_of_contours_with_desired_order(areas_cnt_text_d, index_con_parents_d) + areas_cnt_text_d = self.return_list_of_contours_with_desired_order( + areas_cnt_text_d, index_con_parents_d) cx_bigest_d_big, cy_biggest_d_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest_d]) cx_bigest_d, cy_biggest_d, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent_d) @@ -4613,12 +4571,16 @@ class Eynollah: if len(cx_bigest_d) >= 5: cx_bigest_d_last5 = cx_bigest_d[-5:] cy_biggest_d_last5 = cy_biggest_d[-5:] - dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) for j in range(len(cy_biggest_d_last5))] + dists_d = [math.sqrt((cx_bigest_big[0] - cx_bigest_d_last5[j]) ** 2 + + (cy_biggest_big[0] - cy_biggest_d_last5[j]) ** 2) + for j in range(len(cy_biggest_d_last5))] ind_largest = len(cx_bigest_d) -5 + np.argmin(dists_d) else: cx_bigest_d_last5 = cx_bigest_d[-len(cx_bigest_d):] cy_biggest_d_last5 = cy_biggest_d[-len(cx_bigest_d):] - dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) for j in range(len(cy_biggest_d_last5))] + dists_d = [math.sqrt((cx_bigest_big[0]-cx_bigest_d_last5[j])**2 + + (cy_biggest_big[0]-cy_biggest_d_last5[j])**2) + for j in range(len(cy_biggest_d_last5))] ind_largest = len(cx_bigest_d) - len(cx_bigest_d) + np.argmin(dists_d) cx_bigest_d_big[0] = cx_bigest_d[ind_largest] @@ -4639,7 +4601,9 @@ class Eynollah: p = np.dot(M_22, [cx_bigest[i], cy_biggest[i]]) p[0] = p[0] - x_diff[0] p[1] = p[1] - y_diff[0] - dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + (p[1] - cy_biggest_d[j]) ** 2) for j in range(len(cx_bigest_d))] + dists = [math.sqrt((p[0] - cx_bigest_d[j]) ** 2 + + (p[1] - cy_biggest_d[j]) ** 2) + for j in range(len(cx_bigest_d))] contours_only_text_parent_d_ordered.append(contours_only_text_parent_d[np.argmin(dists)]) # img2=np.zeros((text_only.shape[0],text_only.shape[1],3)) # img2=cv2.fillPoly(img2,pts=[contours_only_text_parent_d[np.argmin(dists)]] ,color=(1,1,1)) @@ -4659,9 +4623,17 @@ class Eynollah: # stop early empty_marginals = [[]] * len(polygons_of_marginals) if self.full_layout: - pcgts = self.writer.build_pagexml_full_layout([], [], page_coord, [], [], [], [], [], [], polygons_of_images, contours_tables, [], polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], cont_page, polygons_lines_xml, []) + pcgts = self.writer.build_pagexml_full_layout( + [], [], page_coord, [], [], [], [], [], [], + polygons_of_images, contours_tables, [], + polygons_of_marginals, empty_marginals, empty_marginals, [], [], [], + cont_page, polygons_lines_xml, []) else: - pcgts = self.writer.build_pagexml_no_full_layout([], page_coord, [], [], [], [], polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) + pcgts = self.writer.build_pagexml_no_full_layout( + [], page_coord, [], [], [], [], + polygons_of_images, + polygons_of_marginals, empty_marginals, empty_marginals, [], [], + cont_page, polygons_lines_xml, contours_tables, []) self.logger.info("Job done in %.1fs", time.time() - t0) if self.dir_in: self.writer.write_pagexml(pcgts) @@ -4671,14 +4643,18 @@ class Eynollah: #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: - contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one(contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + contours_only_text_parent = self.dilate_textregions_contours( + contours_only_text_parent) + contours_only_text_parent = self.filter_contours_inside_a_bigger_one( + contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) #print("text region early 3.5 in %.1fs", time.time() - t0) - txt_con_org = get_textregion_contours_in_org_image_light(contours_only_text_parent, self.image, slope_first, map=self.executor.map) + txt_con_org = get_textregion_contours_in_org_image_light( + contours_only_text_parent, self.image, slope_first, map=self.executor.map) #txt_con_org = self.dilate_textregions_contours(txt_con_org) #contours_only_text_parent = self.dilate_textregions_contours(contours_only_text_parent) else: - txt_con_org = get_textregion_contours_in_org_image(contours_only_text_parent, self.image, slope_first) + txt_con_org = get_textregion_contours_in_org_image( + contours_only_text_parent, self.image, slope_first) #print("text region early 4 in %.1fs", time.time() - t0) boxes_text, _ = get_text_region_boxes_by_given_contours(contours_only_text_parent) boxes_marginals, _ = get_text_region_boxes_by_given_contours(polygons_of_marginals) @@ -4687,59 +4663,84 @@ class Eynollah: if not self.curved_line: if self.light_version: if self.textline_light: - #all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - # self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light2(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light2(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, image_page_rotated, boxes_marginals, slope_deskew) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, \ + all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_light2( + txt_con_org, contours_only_text_parent, textline_mask_tot_ea_org, + image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, \ + all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_light2( + polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_org, + image_page_rotated, boxes_marginals, slope_deskew) #slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con = \ - # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) - + # self.delete_regions_without_textlines(slopes, all_found_textline_polygons, + # boxes_text, txt_con_org, contours_only_text_parent, index_by_text_par_con) #slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, _ = \ - # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) + # self.delete_regions_without_textlines(slopes_marginals, all_found_textline_polygons_marginals, + # boxes_marginals, polygons_of_marginals, polygons_of_marginals, np.array(range(len(polygons_of_marginals)))) #all_found_textline_polygons = self.dilate_textlines(all_found_textline_polygons) #####all_found_textline_polygons = self.dilate_textline_contours(all_found_textline_polygons) - all_found_textline_polygons = self.dilate_textregions_contours_textline_version(all_found_textline_polygons) - all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") - all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version(all_found_textline_polygons_marginals) - - contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, index_by_text_par_con = self.filter_contours_without_textline_inside(contours_only_text_parent,txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered) - + all_found_textline_polygons = self.dilate_textregions_contours_textline_version( + all_found_textline_polygons) + all_found_textline_polygons = self.filter_contours_inside_a_bigger_one( + all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version( + all_found_textline_polygons_marginals) + contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, \ + index_by_text_par_con = self.filter_contours_without_textline_inside( + contours_only_text_parent, txt_con_org, all_found_textline_polygons, + contours_only_text_parent_d_ordered) else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_light(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_light(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) - - #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one(all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, \ + index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_light( + txt_con_org, contours_only_text_parent, textline_mask_tot_ea, + image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, \ + all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_light( + polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, + image_page_rotated, boxes_marginals, slope_deskew) + #all_found_textline_polygons = self.filter_contours_inside_a_bigger_one( + # all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new(txt_con_org, contours_only_text_parent, textline_mask_tot_ea, image_page_rotated, boxes_text, slope_deskew) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, image_page_rotated, boxes_marginals, slope_deskew) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, \ + all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new( + txt_con_org, contours_only_text_parent, textline_mask_tot_ea, + image_page_rotated, boxes_text, slope_deskew) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, \ + all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new( + polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea, + image_page_rotated, boxes_marginals, slope_deskew) else: scale_param = 1 textline_mask_tot_ea_erode = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=2) - all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, index_by_text_par_con, slopes = \ - self.get_slopes_and_deskew_new_curved(txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, image_page_rotated, boxes_text, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons = small_textlines_to_parent_adherence2(all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) - all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, all_box_coord_marginals, _, slopes_marginals = \ - self.get_slopes_and_deskew_new_curved(polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, image_page_rotated, boxes_marginals, text_only, num_col_classifier, scale_param, slope_deskew) - all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2(all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, \ + all_box_coord, index_by_text_par_con, slopes = self.get_slopes_and_deskew_new_curved( + txt_con_org, contours_only_text_parent, textline_mask_tot_ea_erode, + image_page_rotated, boxes_text, text_only, + num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons = small_textlines_to_parent_adherence2( + all_found_textline_polygons, textline_mask_tot_ea, num_col_classifier) + all_found_textline_polygons_marginals, boxes_marginals, _, polygons_of_marginals, \ + all_box_coord_marginals, _, slopes_marginals = self.get_slopes_and_deskew_new_curved( + polygons_of_marginals, polygons_of_marginals, textline_mask_tot_ea_erode, + image_page_rotated, boxes_marginals, text_only, + num_col_classifier, scale_param, slope_deskew) + all_found_textline_polygons_marginals = small_textlines_to_parent_adherence2( + all_found_textline_polygons_marginals, textline_mask_tot_ea, num_col_classifier) #print("text region early 6 in %.1fs", time.time() - t0) if self.full_layout: if np.abs(slope_deskew) >= SLOPE_THRESHOLD: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order( + contours_only_text_parent_d_ordered, index_by_text_par_con) #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + #contours_only_text_parent_d_ordered = \ + #list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #contours_only_text_parent_d_ordered = \ + #list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) else: #takes long timee contours_only_text_parent_d_ordered = None @@ -4749,8 +4750,9 @@ class Eynollah: fun = check_any_text_region_in_model_one_is_main_or_header text_regions_p, contours_only_text_parent, contours_only_text_parent_h, all_box_coord, all_box_coord_h, \ all_found_textline_polygons, all_found_textline_polygons_h, slopes, slopes_h, \ - contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = \ - fun(text_regions_p, regions_fully, contours_only_text_parent, all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) + contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered = fun( + text_regions_p, regions_fully, contours_only_text_parent, + all_box_coord, all_found_textline_polygons, slopes, contours_only_text_parent_d_ordered) if self.plotter: self.plotter.save_plot_of_layout(text_regions_p, image_page) @@ -4758,60 +4760,76 @@ class Eynollah: pixel_img = 4 polygons_of_drop_capitals = return_contours_of_interested_region_by_min_size(text_regions_p, pixel_img) - all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline(text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, - all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, - kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) - pixel_lines = 6 + all_found_textline_polygons = adhere_drop_capital_region_into_corresponding_textline( + text_regions_p, polygons_of_drop_capitals, contours_only_text_parent, contours_only_text_parent_h, + all_box_coord, all_box_coord_h, all_found_textline_polygons, all_found_textline_polygons_h, + kernel=KERNEL, curved_line=self.curved_line, textline_light=self.textline_light) if not self.reading_order_machine_based: + pixel_seps = 6 if not self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_seps, contours_only_text_parent_h) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines, contours_only_text_parent_h_d_ordered) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_seps, contours_only_text_parent_h_d_ordered) elif self.headers_off: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document(np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + num_col, _, matrix_of_lines_ch, splitter_y_new, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_seps) else: - _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document(np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), num_col_classifier, self.tables, pixel_lines) + _, _, matrix_of_lines_ch_d, splitter_y_new_d, _ = find_number_of_columns_in_document( + np.repeat(text_regions_p_1_n[:, :, np.newaxis], 3, axis=2), + num_col_classifier, self.tables, pixel_seps) if num_col_classifier >= 3: if np.abs(slope_deskew) < SLOPE_THRESHOLD: regions_without_separators = regions_without_separators.astype(np.uint8) regions_without_separators = cv2.erode(regions_without_separators[:, :], KERNEL, iterations=6) - else: regions_without_separators_d = regions_without_separators_d.astype(np.uint8) regions_without_separators_d = cv2.erode(regions_without_separators_d[:, :], KERNEL, iterations=6) if np.abs(slope_deskew) < SLOPE_THRESHOLD: - boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_without_separators, matrix_of_lines_ch, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes, peaks_neg_tot_tables = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new, regions_without_separators, matrix_of_lines_ch, + num_col_classifier, erosion_hurts, self.tables, self.right2left) else: - boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new(splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, num_col_classifier, erosion_hurts, self.tables, self.right2left) + boxes_d, peaks_neg_tot_tables_d = return_boxes_of_images_by_order_of_reading_new( + splitter_y_new_d, regions_without_separators_d, matrix_of_lines_ch_d, + num_col_classifier, erosion_hurts, self.tables, self.right2left) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) t_order = time.time() if self.full_layout: - if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model( + contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + order_text_new, id_of_texts_tot = self.do_order_of_regions( + contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) + order_text_new, id_of_texts_tot = self.do_order_of_regions( + contours_only_text_parent_d_ordered, contours_only_text_parent_h_d_ordered, boxes_d, textline_mask_tot_d) self.logger.info("detection of reading order took %.1fs", time.time() - t_order) if self.ocr: ocr_all_textlines = [] else: ocr_all_textlines = None - pcgts = self.writer.build_pagexml_full_layout(contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, - all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, - cont_page, polygons_lines_xml, ocr_all_textlines) + pcgts = self.writer.build_pagexml_full_layout( + contours_only_text_parent, contours_only_text_parent_h, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, all_found_textline_polygons_h, all_box_coord, all_box_coord_h, + polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, + cont_page, polygons_lines_xml, ocr_all_textlines) self.logger.info("Job done in %.1fs", time.time() - t0) #print("Job done in %.1fs", time.time() - t0) if self.dir_in: @@ -4823,21 +4841,25 @@ class Eynollah: else: contours_only_text_parent_h = None if self.reading_order_machine_based: - order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model(contours_only_text_parent, contours_only_text_parent_h, text_regions_p) + order_text_new, id_of_texts_tot = self.do_order_of_regions_with_model( + contours_only_text_parent, contours_only_text_parent_h, text_regions_p) else: if np.abs(slope_deskew) < SLOPE_THRESHOLD: - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) + order_text_new, id_of_texts_tot = self.do_order_of_regions( + contours_only_text_parent, contours_only_text_parent_h, boxes, textline_mask_tot) else: - contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order(contours_only_text_parent_d_ordered, index_by_text_par_con) + contours_only_text_parent_d_ordered = self.return_list_of_contours_with_desired_order( + contours_only_text_parent_d_ordered, index_by_text_par_con) #try: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) + #contours_only_text_parent_d_ordered = \ + #list(np.array(contours_only_text_parent_d_ordered, dtype=object)[index_by_text_par_con]) #except: - #contours_only_text_parent_d_ordered = list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) - order_text_new, id_of_texts_tot = self.do_order_of_regions(contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) - + #contours_only_text_parent_d_ordered = \ + #list(np.array(contours_only_text_parent_d_ordered, dtype=np.int32)[index_by_text_par_con]) + order_text_new, id_of_texts_tot = self.do_order_of_regions( + contours_only_text_parent_d_ordered, contours_only_text_parent_h, boxes_d, textline_mask_tot_d) if self.ocr: - device = cuda.get_current_device() device.reset() gc.collect() @@ -4849,7 +4871,6 @@ class Eynollah: ind_tot = 0 #cv2.imwrite('./img_out.png', image_page) - ocr_all_textlines = [] for indexing, ind_poly_first in enumerate(all_found_textline_polygons): ocr_textline_in_textregion = [] @@ -4871,7 +4892,6 @@ class Eynollah: img_poly_on_img = np.copy(image_page) else: img_poly_on_img = np.copy(img_bin_light) - mask_poly = cv2.fillPoly(mask_poly, pts=[ind_poly], color=(1, 1, 1)) if self.textline_light: @@ -4883,10 +4903,7 @@ class Eynollah: img_croped = img_poly_on_img[y:y+h, x:x+w, :] #cv2.imwrite('./extracted_lines/'+str(ind_tot)+'.jpg', img_croped) text_ocr = self.return_ocr_of_textline_without_common_section(img_croped, model_ocr, processor, device, w, h2w_ratio, ind_tot) - ocr_textline_in_textregion.append(text_ocr) - - ind_tot = ind_tot +1 ocr_all_textlines.append(ocr_textline_in_textregion) @@ -4894,9 +4911,11 @@ class Eynollah: ocr_all_textlines = None #print(ocr_all_textlines) self.logger.info("detection of reading order took %.1fs", time.time() - t_order) - pcgts = self.writer.build_pagexml_no_full_layout(txt_con_org, page_coord, order_text_new, id_of_texts_tot, all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, - all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, - cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) + pcgts = self.writer.build_pagexml_no_full_layout( + txt_con_org, page_coord, order_text_new, id_of_texts_tot, + all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, + all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, + cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) #print("Job done in %.1fs" % (time.time() - t0)) self.logger.info("Job done in %.1fs", time.time() - t0) if not self.dir_in: diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index d7f9ccd..feab341 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -14,9 +14,9 @@ from .contour import (contours_in_same_horizon, return_contours_of_image, return_parent_contours) -def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peak_points,cy_hor_diff): - - +def return_x_start_end_mothers_childs_and_type_of_reading_order( + x_min_hor_some, x_max_hor_some, cy_hor_some, peak_points, cy_hor_diff): + x_start=[] x_end=[] kind=[]#if covers 2 and more than 2 columns set it to 1 otherwise 0 @@ -30,15 +30,12 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x starting=x_min_hor_some[i]-peak_points starting=starting[starting>=0] min_start=np.argmin(starting) - - ending=peak_points-x_max_hor_some[i] len_ending_neg=len(ending[ending<=0]) ending=ending[ending>0] max_end=np.argmin(ending)+len_ending_neg - if (max_end-min_start)>=2: if (max_end-min_start)==(len(peak_points)-1): new_main_sep_y.append(indexer) @@ -57,18 +54,13 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x kind.append(1) indexer+=1 + + x_start_returned = np.array(x_start, dtype=int) + x_end_returned = np.array(x_end, dtype=int) + y_sep_returned = np.array(y_sep, dtype=int) + y_diff_returned = np.array(y_diff, dtype=int) - - x_start_returned=np.copy(x_start) - x_end_returned=np.copy(x_end) - y_sep_returned=np.copy(y_sep) - y_diff_returned=np.copy(y_diff) - - - - - all_args_uniq=contours_in_same_horizon(y_sep_returned) - + all_args_uniq = contours_in_same_horizon(y_sep_returned) args_to_be_unified=[] y_unified=[] y_diff_unified=[] @@ -84,7 +76,10 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x y_sep_same_hor=np.array(y_sep_returned)[all_args_uniq[dd]] y_diff_same_hor=np.array(y_diff_returned)[all_args_uniq[dd]] #print('burda2') - if x_s_same_hor[0]==(x_e_same_hor[1]-1) or x_s_same_hor[1]==(x_e_same_hor[0]-1) and x_s_same_hor[0]!=x_s_same_hor[1] and x_e_same_hor[0]!=x_e_same_hor[1]: + if (x_s_same_hor[0]==x_e_same_hor[1]-1 or + x_s_same_hor[1]==x_e_same_hor[0]-1 and + x_s_same_hor[0]!=x_s_same_hor[1] and + x_e_same_hor[0]!=x_e_same_hor[1]): #print('burda3') for arg_in in all_args_uniq[dd]: #print(arg_in,'arg_in') @@ -98,19 +93,14 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x x_e_unified.append(x_e_selected) y_unified.append(y_selected) y_diff_unified.append(y_diff_selected) - - - #print(x_s_same_hor,'x_s_same_hor') #print(x_e_same_hor[:]-1,'x_e_same_hor') #print('#############################') - #print(x_s_unified,'y_selected') #print(x_e_unified,'x_s_selected') #print(y_unified,'x_e_same_hor') - + args_lines_not_unified=list( set(range(len(y_sep_returned)))-set(args_to_be_unified) ) - #print(args_lines_not_unified,'args_lines_not_unified') x_start_returned_not_unified=list( np.array(x_start_returned)[args_lines_not_unified] ) @@ -128,11 +118,10 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x #print(x_start_returned,'x_start_returned') #print(x_end_returned,'x_end_returned') - x_start_returned=np.copy(x_start_returned_not_unified) - x_end_returned=np.copy(x_end_returned_not_unified) - y_sep_returned=np.copy(y_sep_returned_not_unified) - y_diff_returned=np.copy(y_diff_returned_not_unified) - + x_start_returned = np.array(x_start_returned_not_unified, dtype=int) + x_end_returned = np.array(x_end_returned_not_unified, dtype=int) + y_sep_returned = np.array(y_sep_returned_not_unified, dtype=int) + y_diff_returned = np.array(y_diff_returned_not_unified, dtype=int) #print(y_sep_returned,'y_sep_returned2') #print(x_start_returned,'x_start_returned2') @@ -165,19 +154,19 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x #print(y_min_new,'y_min_new') #print(y_max_new,'y_max_new') - - #print(y_sep[new_main_sep_y[0]],y_sep,'yseps') x_start=np.array(x_start) x_end=np.array(x_end) kind=np.array(kind) y_sep=np.array(y_sep) - if (y_min_new in y_mains_sep_ohne_grenzen) and (y_max_new in y_mains_sep_ohne_grenzen): + if (y_min_new in y_mains_sep_ohne_grenzen and + y_max_new in y_mains_sep_ohne_grenzen): x_start=x_start[(y_sep>y_min_new) & (y_sepy_min_new) & (y_sepy_min_new) & (y_sepy_min_new) & (y_sepy_min_new) & (y_sep<=y_max_new)] #print('burda1') @@ -185,7 +174,8 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x #print('burda2') kind=kind[(y_sep>y_min_new) & (y_sep<=y_max_new)] y_sep=y_sep[(y_sep>y_min_new) & (y_sep<=y_max_new)] - elif (y_min_new not in y_mains_sep_ohne_grenzen) and (y_max_new in y_mains_sep_ohne_grenzen): + elif (y_min_new not in y_mains_sep_ohne_grenzen and + y_max_new in y_mains_sep_ohne_grenzen): x_start=x_start[(y_sep>=y_min_new) & (y_sep=y_min_new) & (y_sep=y_min_new) & (y_sep1: #print(np.array(remained_sep_indexes),'np.array(remained_sep_indexes)') #print(np.array(mother),'mother') - remained_sep_indexes_without_mother=np.array(list(remained_sep_indexes))[np.array(mother)==0] - remained_sep_indexes_with_child_without_mother=np.array(list(remained_sep_indexes))[(np.array(mother)==0) & (np.array(child)==1)] + remained_sep_indexes_without_mother = remained_sep_indexes[mother==0] + remained_sep_indexes_with_child_without_mother = remained_sep_indexes[mother==0 & child==1] #print(remained_sep_indexes_without_mother,'remained_sep_indexes_without_mother') - - - x_end_with_child_without_mother=np.array(x_end)[np.array(remained_sep_indexes_with_child_without_mother)] - - x_start_with_child_without_mother=np.array(x_start)[np.array(remained_sep_indexes_with_child_without_mother)] - - y_lines_with_child_without_mother=np.array(y_sep)[np.array(remained_sep_indexes_with_child_without_mother)] - - + x_end_with_child_without_mother = x_end[remained_sep_indexes_with_child_without_mother] + x_start_with_child_without_mother = x_start[remained_sep_indexes_with_child_without_mother] + y_lines_with_child_without_mother = y_sep[remained_sep_indexes_with_child_without_mother] + reading_orther_type=0 - - - x_end_without_mother=np.array(x_end)[np.array(remained_sep_indexes_without_mother)] - x_start_without_mother=np.array(x_start)[np.array(remained_sep_indexes_without_mother)] - y_lines_without_mother=np.array(y_sep)[np.array(remained_sep_indexes_without_mother)] + x_end_without_mother = x_end[remained_sep_indexes_without_mother] + x_start_without_mother = x_start[remained_sep_indexes_without_mother] + y_lines_without_mother = y_sep[remained_sep_indexes_without_mother] if len(remained_sep_indexes_without_mother)>=2: for i in range(len(remained_sep_indexes_without_mother)-1): - ##nodes_i=set(range(x_start[remained_sep_indexes_without_mother[i]],x_end[remained_sep_indexes_without_mother[i]]+1)) - nodes_i=set(range(x_start[remained_sep_indexes_without_mother[i]],x_end[remained_sep_indexes_without_mother[i]])) + nodes_i=set(range(x_start[remained_sep_indexes_without_mother[i]], + x_end[remained_sep_indexes_without_mother[i]] + # + 1 + )) for j in range(i+1,len(remained_sep_indexes_without_mother)): - #nodes_j=set(range(x_start[remained_sep_indexes_without_mother[j]],x_end[remained_sep_indexes_without_mother[j]]+1)) - nodes_j=set(range(x_start[remained_sep_indexes_without_mother[j]],x_end[remained_sep_indexes_without_mother[j]])) - - set_diff=nodes_i-nodes_j - - if set_diff!=nodes_i: - reading_orther_type=1 + nodes_j=set(range(x_start[remained_sep_indexes_without_mother[j]], + x_end[remained_sep_indexes_without_mother[j]] + # + 1 + )) + set_diff = nodes_i - nodes_j + if set_diff != nodes_i: + reading_orther_type = 1 else: - reading_orther_type=0 + reading_orther_type = 0 #print(reading_orther_type,'javab') - #print(y_lines_with_child_without_mother,'y_lines_with_child_without_mother') #print(x_start_with_child_without_mother,'x_start_with_child_without_mother') #print(x_end_with_child_without_mother,'x_end_with_hild_without_mother') - len_sep_with_child=len(np.array(child)[np.array(child)==1]) + len_sep_with_child = len(child[child==1]) #print(len_sep_with_child,'len_sep_with_child') - there_is_sep_with_child=0 - - if len_sep_with_child>=1: - there_is_sep_with_child=1 - + there_is_sep_with_child = 0 + if len_sep_with_child >= 1: + there_is_sep_with_child = 1 #print(all_args_uniq,'all_args_uniq') #print(args_to_be_unified,'args_to_be_unified') - - return reading_orther_type,x_start_returned, x_end_returned ,y_sep_returned,y_diff_returned,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y + return (reading_orther_type, + x_start_returned, + x_end_returned, + y_sep_returned, + y_diff_returned, + y_lines_without_mother, + x_start_without_mother, + x_end_without_mother, + there_is_sep_with_child, + y_lines_with_child_without_mother, + x_start_with_child_without_mother, + x_end_with_child_without_mother, + new_main_sep_y) + def crop_image_inside_box(box, img_org_copy): image_box = img_org_copy[box[1] : box[1] + box[3], box[0] : box[0] + box[2]] return image_box, [box[1], box[1] + box[3], box[0], box[0] + box[2]] @@ -304,7 +303,6 @@ def otsu_copy_binary(img): img1 = img[:, :, 0] retval1, threshold1 = cv2.threshold(img1, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) - img_r[:, :, 0] = threshold1 img_r[:, :, 1] = threshold1 img_r[:, :, 2] = threshold1 @@ -312,9 +310,7 @@ def otsu_copy_binary(img): img_r = img_r / float(np.max(img_r)) * 255 return img_r - def find_features_of_lines(contours_main): - areas_main = np.array([cv2.contourArea(contours_main[j]) for j in range(len(contours_main))]) M_main = [cv2.moments(contours_main[j]) for j in range(len(contours_main))] cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] @@ -326,7 +322,6 @@ def find_features_of_lines(contours_main): y_max_main = np.array([np.max(contours_main[j][:, 0, 1]) for j in range(len(contours_main))]) slope_lines = [] - for kk in range(len(contours_main)): [vx, vy, x, y] = cv2.fitLine(contours_main[kk], cv2.DIST_L2, 0, 0.01, 0.01) slope_lines.append(((vy / vx) / np.pi * 180)[0]) @@ -339,29 +334,42 @@ def find_features_of_lines(contours_main): slope_lines[(slope_lines != 0) & (slope_lines != 1)] = 2 dis_x = np.abs(x_max_main - x_min_main) - return slope_lines, dis_x, x_min_main, x_max_main, np.array(cy_main), np.array(slope_lines_org), y_min_main, y_max_main, np.array(cx_main) + return (slope_lines, + dis_x, + x_min_main, + x_max_main, + np.array(cy_main), + np.array(slope_lines_org), + y_min_main, + y_max_main, + np.array(cx_main)) def boosting_headers_by_longshot_region_segmentation(textregion_pre_p, textregion_pre_np, img_only_text): textregion_pre_p_org = np.copy(textregion_pre_p) # 4 is drop capitals - headers_in_longshot = (textregion_pre_np[:, :, 0] == 2) * 1 - # headers_in_longshot= ( (textregion_pre_np[:,:,0]==2) | (textregion_pre_np[:,:,0]==1) )*1 - textregion_pre_p[:, :, 0][(headers_in_longshot[:, :] == 1) & (textregion_pre_p[:, :, 0] != 4)] = 2 + headers_in_longshot = textregion_pre_np[:, :, 0] == 2 + #headers_in_longshot = ((textregion_pre_np[:,:,0]==2) | + # (textregion_pre_np[:,:,0]==1)) + textregion_pre_p[:, :, 0][headers_in_longshot & + (textregion_pre_p[:, :, 0] != 4)] = 2 textregion_pre_p[:, :, 0][textregion_pre_p[:, :, 0] == 1] = 0 # earlier it was so, but by this manner the drop capitals are also deleted - # textregion_pre_p[:,:,0][( img_only_text[:,:]==1) & (textregion_pre_p[:,:,0]!=7) & (textregion_pre_p[:,:,0]!=2)]=1 - textregion_pre_p[:, :, 0][(img_only_text[:, :] == 1) & (textregion_pre_p[:, :, 0] != 7) & (textregion_pre_p[:, :, 0] != 4) & (textregion_pre_p[:, :, 0] != 2)] = 1 + # textregion_pre_p[:,:,0][(img_only_text[:,:]==1) & + # (textregion_pre_p[:,:,0]!=7) & + # (textregion_pre_p[:,:,0]!=2)] = 1 + textregion_pre_p[:, :, 0][(img_only_text[:, :] == 1) & + (textregion_pre_p[:, :, 0] != 7) & + (textregion_pre_p[:, :, 0] != 4) & + (textregion_pre_p[:, :, 0] != 2)] = 1 return textregion_pre_p - def find_num_col_deskew(regions_without_separators, sigma_, multiplier=3.8): - regions_without_separators_0 = regions_without_separators[:,:].sum(axis=1) + regions_without_separators_0 = regions_without_separators.sum(axis=1) z = gaussian_filter1d(regions_without_separators_0, sigma_) return np.std(z) - def find_num_col(regions_without_separators, num_col_classifier, tables, multiplier=3.8): - regions_without_separators_0 = regions_without_separators[:, :].sum(axis=0) + regions_without_separators_0 = regions_without_separators.sum(axis=0) ##plt.plot(regions_without_separators_0) ##plt.show() sigma_ = 35 # 70#35 @@ -372,7 +380,7 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl y = regions_without_separators_0 # [first_nonzero:last_nonzero] y_help = np.zeros(len(y) + 20) y_help[10 : len(y) + 10] = y - x = np.array(range(len(y))) + x = np.arange(len(y)) zneg_rev = -y_help + np.max(y_help) zneg = np.zeros(len(zneg_rev) + 20) zneg[10 : len(zneg_rev) + 10] = zneg_rev @@ -386,9 +394,12 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl last_nonzero = last_nonzero - 100 first_nonzero = first_nonzero + 200 - peaks_neg = peaks_neg[(peaks_neg > first_nonzero) & (peaks_neg < last_nonzero)] - peaks = peaks[(peaks > 0.06 * regions_without_separators.shape[1]) & (peaks < 0.94 * regions_without_separators.shape[1])] - peaks_neg = peaks_neg[(peaks_neg > 370) & (peaks_neg < (regions_without_separators.shape[1] - 370))] + peaks_neg = peaks_neg[(peaks_neg > first_nonzero) & + (peaks_neg < last_nonzero)] + peaks = peaks[(peaks > 0.06 * regions_without_separators.shape[1]) & + (peaks < 0.94 * regions_without_separators.shape[1])] + peaks_neg = peaks_neg[(peaks_neg > 370) & + (peaks_neg < (regions_without_separators.shape[1] - 370))] interest_pos = z[peaks] interest_pos = interest_pos[interest_pos > 10] # plt.plot(z) @@ -405,7 +416,8 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl # print(np.min(interest_pos),np.max(interest_pos),np.max(interest_pos)/np.min(interest_pos),'minmax') dis_talaei = (min_peaks_pos - min_peaks_neg) / multiplier - grenze = min_peaks_pos - dis_talaei # np.mean(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])-np.std(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])/2.0 + grenze = min_peaks_pos - dis_talaei + # np.mean(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])-np.std(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])/2.0 # print(interest_neg,'interest_neg') # print(grenze,'grenze') @@ -441,19 +453,26 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl p_g_u = len(y) - int(len(y) / 4.0) if num_col == 3: - if (peaks_neg_fin[0] > p_g_u and peaks_neg_fin[1] > p_g_u) or (peaks_neg_fin[0] < p_g_l and peaks_neg_fin[1] < p_g_l) or ((peaks_neg_fin[0] + 200) < p_m and peaks_neg_fin[1] < p_m) or ((peaks_neg_fin[0] - 200) > p_m and peaks_neg_fin[1] > p_m): + if ((peaks_neg_fin[0] > p_g_u and + peaks_neg_fin[1] > p_g_u) or + (peaks_neg_fin[0] < p_g_l and + peaks_neg_fin[1] < p_g_l) or + (peaks_neg_fin[0] + 200 < p_m and + peaks_neg_fin[1] < p_m) or + (peaks_neg_fin[0] - 200 > p_m and + peaks_neg_fin[1] > p_m)): num_col = 1 peaks_neg_fin = [] if num_col == 2: - if (peaks_neg_fin[0] > p_g_u) or (peaks_neg_fin[0] < p_g_l): + if (peaks_neg_fin[0] > p_g_u or + peaks_neg_fin[0] < p_g_l): num_col = 1 peaks_neg_fin = [] ##print(len(peaks_neg_fin)) diff_peaks = np.abs(np.diff(peaks_neg_fin)) - cut_off = 400 peaks_neg_true = [] forest = [] @@ -489,23 +508,35 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl ##print(num_col,'early') if num_col == 3: - if (peaks_neg_true[0] > p_g_u and peaks_neg_true[1] > p_g_u) or (peaks_neg_true[0] < p_g_l and peaks_neg_true[1] < p_g_l) or (peaks_neg_true[0] < p_m and (peaks_neg_true[1] + 200) < p_m) or ((peaks_neg_true[0] - 200) > p_m and peaks_neg_true[1] > p_m): + if ((peaks_neg_true[0] > p_g_u and + peaks_neg_true[1] > p_g_u) or + (peaks_neg_true[0] < p_g_l and + peaks_neg_true[1] < p_g_l) or + (peaks_neg_true[0] < p_m and + peaks_neg_true[1] + 200 < p_m) or + (peaks_neg_true[0] - 200 > p_m and + peaks_neg_true[1] > p_m)): num_col = 1 peaks_neg_true = [] - elif (peaks_neg_true[0] < p_g_u and peaks_neg_true[0] > p_g_l) and (peaks_neg_true[1] > p_u_quarter): + elif (peaks_neg_true[0] < p_g_u and + peaks_neg_true[0] > p_g_l and + peaks_neg_true[1] > p_u_quarter): peaks_neg_true = [peaks_neg_true[0]] - elif (peaks_neg_true[1] < p_g_u and peaks_neg_true[1] > p_g_l) and (peaks_neg_true[0] < p_quarter): + elif (peaks_neg_true[1] < p_g_u and + peaks_neg_true[1] > p_g_l and + peaks_neg_true[0] < p_quarter): peaks_neg_true = [peaks_neg_true[1]] if num_col == 2: - if (peaks_neg_true[0] > p_g_u) or (peaks_neg_true[0] < p_g_l): + if (peaks_neg_true[0] > p_g_u or + peaks_neg_true[0] < p_g_l): num_col = 1 peaks_neg_true = [] diff_peaks_abnormal = diff_peaks[diff_peaks < 360] if len(diff_peaks_abnormal) > 0: - arg_help = np.array(range(len(diff_peaks))) + arg_help = np.arange(len(diff_peaks)) arg_help_ann = arg_help[diff_peaks < 360] peaks_neg_fin_new = [] @@ -527,7 +558,6 @@ def find_num_col(regions_without_separators, num_col_classifier, tables, multipl # plt.plot(peaks_neg_true,z[peaks_neg_true],'*') # plt.plot([0,len(y)], [grenze,grenze]) # plt.show() - ##print(len(peaks_neg_true)) return len(peaks_neg_true), peaks_neg_true @@ -536,7 +566,6 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): ##plt.plot(regions_without_separators_0) ##plt.show() - sigma_ = 15 meda_n_updown = regions_without_separators_0[len(regions_without_separators_0) :: -1] @@ -547,32 +576,24 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): last_nonzero = len(regions_without_separators_0) - last_nonzero y = regions_without_separators_0 # [first_nonzero:last_nonzero] - y_help = np.zeros(len(y) + 20) - y_help[10 : len(y) + 10] = y - - x = np.array(range(len(y))) + x = np.arange(len(y)) zneg_rev = -y_help + np.max(y_help) - zneg = np.zeros(len(zneg_rev) + 20) - zneg[10 : len(zneg_rev) + 10] = zneg_rev - z = gaussian_filter1d(y, sigma_) zneg = gaussian_filter1d(zneg, sigma_) peaks_neg, _ = find_peaks(zneg, height=0) peaks, _ = find_peaks(z, height=0) - peaks_neg = peaks_neg - 10 - 10 - peaks_neg_org = np.copy(peaks_neg) - - peaks_neg = peaks_neg[(peaks_neg > first_nonzero) & (peaks_neg < last_nonzero)] - - peaks = peaks[(peaks > 0.09 * regions_without_separators.shape[1]) & (peaks < 0.91 * regions_without_separators.shape[1])] + peaks_neg = peaks_neg[(peaks_neg > first_nonzero) & + (peaks_neg < last_nonzero)] + peaks = peaks[(peaks > 0.09 * regions_without_separators.shape[1]) & + (peaks < 0.91 * regions_without_separators.shape[1])] peaks_neg = peaks_neg[(peaks_neg > 500) & (peaks_neg < (regions_without_separators.shape[1] - 500))] # print(peaks) @@ -587,7 +608,8 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): # $print(min_peaks_pos) dis_talaei = (min_peaks_pos - min_peaks_neg) / multiplier # print(interest_pos) - grenze = min_peaks_pos - dis_talaei # np.mean(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])-np.std(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])/2.0 + grenze = min_peaks_pos - dis_talaei + # np.mean(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])-np.std(y[peaks_neg[0]:peaks_neg[len(peaks_neg)-1]])/2.0 interest_neg_fin = interest_neg[(interest_neg < grenze)] peaks_neg_fin = peaks_neg[(interest_neg < grenze)] @@ -601,13 +623,21 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): p_g_u = len(y) - int(len(y) / 3.0) if num_col == 3: - if (peaks_neg_fin[0] > p_g_u and peaks_neg_fin[1] > p_g_u) or (peaks_neg_fin[0] < p_g_l and peaks_neg_fin[1] < p_g_l) or (peaks_neg_fin[0] < p_m and peaks_neg_fin[1] < p_m) or (peaks_neg_fin[0] > p_m and peaks_neg_fin[1] > p_m): + if ((peaks_neg_fin[0] > p_g_u and + peaks_neg_fin[1] > p_g_u) or + (peaks_neg_fin[0] < p_g_l and + peaks_neg_fin[1] < p_g_l) or + (peaks_neg_fin[0] < p_m and + peaks_neg_fin[1] < p_m) or + (peaks_neg_fin[0] > p_m and + peaks_neg_fin[1] > p_m)): num_col = 1 else: pass if num_col == 2: - if (peaks_neg_fin[0] > p_g_u) or (peaks_neg_fin[0] < p_g_l): + if (peaks_neg_fin[0] > p_g_u or + peaks_neg_fin[0] < p_g_l): num_col = 1 else: pass @@ -646,23 +676,36 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): p_u_quarter = len(y) - p_quarter if num_col == 3: - if (peaks_neg_true[0] > p_g_u and peaks_neg_true[1] > p_g_u) or (peaks_neg_true[0] < p_g_l and peaks_neg_true[1] < p_g_l) or (peaks_neg_true[0] < p_m and peaks_neg_true[1] < p_m) or (peaks_neg_true[0] > p_m and peaks_neg_true[1] > p_m): + if ((peaks_neg_true[0] > p_g_u and + peaks_neg_true[1] > p_g_u) or + (peaks_neg_true[0] < p_g_l and + peaks_neg_true[1] < p_g_l) or + (peaks_neg_true[0] < p_m and + peaks_neg_true[1] < p_m) or + (peaks_neg_true[0] > p_m and + peaks_neg_true[1] > p_m)): num_col = 1 peaks_neg_true = [] - elif (peaks_neg_true[0] < p_g_u and peaks_neg_true[0] > p_g_l) and (peaks_neg_true[1] > p_u_quarter): + elif (peaks_neg_true[0] < p_g_u and + peaks_neg_true[0] > p_g_l and + peaks_neg_true[1] > p_u_quarter): peaks_neg_true = [peaks_neg_true[0]] - elif (peaks_neg_true[1] < p_g_u and peaks_neg_true[1] > p_g_l) and (peaks_neg_true[0] < p_quarter): + elif (peaks_neg_true[1] < p_g_u and + peaks_neg_true[1] > p_g_l and + peaks_neg_true[0] < p_quarter): peaks_neg_true = [peaks_neg_true[1]] else: pass if num_col == 2: - if (peaks_neg_true[0] > p_g_u) or (peaks_neg_true[0] < p_g_l): + if (peaks_neg_true[0] > p_g_u or + peaks_neg_true[0] < p_g_l): num_col = 1 peaks_neg_true = [] if num_col == 4: - if len(np.array(peaks_neg_true)[np.array(peaks_neg_true) < p_g_l]) == 2 or len(np.array(peaks_neg_true)[np.array(peaks_neg_true) > (len(y) - p_g_l)]) == 2: + if (len(np.array(peaks_neg_true)[np.array(peaks_neg_true) < p_g_l]) == 2 or + len(np.array(peaks_neg_true)[np.array(peaks_neg_true) > (len(y) - p_g_l)]) == 2): num_col = 1 peaks_neg_true = [] else: @@ -674,7 +717,10 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): for i in range(len(peaks_neg_true)): hill_main = peaks_neg_true[i] # deep_depth=z[peaks_neg] - hills_around = peaks_neg_org[((peaks_neg_org > hill_main) & (peaks_neg_org <= hill_main + 400)) | ((peaks_neg_org < hill_main) & (peaks_neg_org >= hill_main - 400))] + hills_around = peaks_neg_org[((peaks_neg_org > hill_main) & + (peaks_neg_org <= hill_main + 400)) | + ((peaks_neg_org < hill_main) & + (peaks_neg_org >= hill_main - 400))] deep_depth_around = z[hills_around] # print(hill_main,z[hill_main],hills_around,deep_depth_around,'manoooo') @@ -687,13 +733,11 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): pass diff_peaks_annormal = diff_peaks[diff_peaks < 360] - if len(diff_peaks_annormal) > 0: - arg_help = np.array(range(len(diff_peaks))) + arg_help = np.arange(len(diff_peaks)) arg_help_ann = arg_help[diff_peaks < 360] peaks_neg_fin_new = [] - for ii in range(len(peaks_neg_fin)): if ii in arg_help_ann: arg_min = np.argmin([interest_neg_fin[ii], interest_neg_fin[ii + 1]]) @@ -701,7 +745,6 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): peaks_neg_fin_new.append(peaks_neg_fin[ii]) else: peaks_neg_fin_new.append(peaks_neg_fin[ii + 1]) - elif (ii - 1) in arg_help_ann: pass else: @@ -711,7 +754,6 @@ def find_num_col_only_image(regions_without_separators, multiplier=3.8): # sometime pages with one columns gives also some negative peaks. delete those peaks param = z[peaks_neg_true] / float(min_peaks_pos) * 100 - if len(param[param <= 41]) == 0: peaks_neg_true = [] @@ -722,11 +764,9 @@ def find_num_col_by_vertical_lines(regions_without_separators, multiplier=3.8): ##plt.plot(regions_without_separators_0) ##plt.show() - sigma_ = 35 # 70#35 z = gaussian_filter1d(regions_without_separators_0, sigma_) - peaks, _ = find_peaks(z, height=0) # print(peaks,'peaksnew') @@ -734,39 +774,43 @@ def find_num_col_by_vertical_lines(regions_without_separators, multiplier=3.8): def return_regions_without_separators(regions_pre): kernel = np.ones((5, 5), np.uint8) - regions_without_separators = ((regions_pre[:, :] != 6) & (regions_pre[:, :] != 0)) * 1 - # regions_without_separators=( (image_regions_eraly_p[:,:,:]!=6) & (image_regions_eraly_p[:,:,:]!=0) & (image_regions_eraly_p[:,:,:]!=5) & (image_regions_eraly_p[:,:,:]!=8) & (image_regions_eraly_p[:,:,:]!=7))*1 + regions_without_separators = ((regions_pre[:, :] != 6) & + (regions_pre[:, :] != 0)) + # regions_without_separators=( (image_regions_eraly_p[:,:,:]!=6) & + # (image_regions_eraly_p[:,:,:]!=0) & + # (image_regions_eraly_p[:,:,:]!=5) & + # (image_regions_eraly_p[:,:,:]!=8) & + # (image_regions_eraly_p[:,:,:]!=7)) - regions_without_separators = regions_without_separators.astype(np.uint8) - - regions_without_separators = cv2.erode(regions_without_separators, kernel, iterations=6) + regions_without_separators = cv2.erode(regions_without_separators.astype(np.uint8), kernel, iterations=6) return regions_without_separators - def put_drop_out_from_only_drop_model(layout_no_patch, layout1): - drop_only = (layout_no_patch[:, :, 0] == 4) * 1 contours_drop, hir_on_drop = return_contours_of_image(drop_only) contours_drop_parent = return_parent_contours(contours_drop, hir_on_drop) - areas_cnt_text = np.array([cv2.contourArea(contours_drop_parent[j]) for j in range(len(contours_drop_parent))]) + areas_cnt_text = np.array([cv2.contourArea(contours_drop_parent[j]) + for j in range(len(contours_drop_parent))]) areas_cnt_text = areas_cnt_text / float(drop_only.shape[0] * drop_only.shape[1]) - - contours_drop_parent = [contours_drop_parent[jz] for jz in range(len(contours_drop_parent)) if areas_cnt_text[jz] > 0.00001] - - areas_cnt_text = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > 0.00001] + contours_drop_parent = [contours_drop_parent[jz] + for jz in range(len(contours_drop_parent)) + if areas_cnt_text[jz] > 0.00001] + areas_cnt_text = [areas_cnt_text[jz] + for jz in range(len(areas_cnt_text)) + if areas_cnt_text[jz] > 0.00001] contours_drop_parent_final = [] - for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) # boxes.append([int(x), int(y), int(w), int(h)]) map_of_drop_contour_bb = np.zeros((layout1.shape[0], layout1.shape[1])) map_of_drop_contour_bb[y : y + h, x : x + w] = layout1[y : y + h, x : x + w] - - if (((map_of_drop_contour_bb == 1) * 1).sum() / float(((map_of_drop_contour_bb == 5) * 1).sum()) * 100) >= 15: + if (100. * + (map_of_drop_contour_bb == 1).sum() / + (map_of_drop_contour_bb == 5).sum()) >= 15: contours_drop_parent_final.append(contours_drop_parent[jj]) layout_no_patch[:, :, 0][layout_no_patch[:, :, 0] == 4] = 0 @@ -780,49 +824,53 @@ def putt_bb_of_drop_capitals_of_model_in_patches_in_layout(layout_in_patch, drop contours_drop, hir_on_drop = return_contours_of_image(drop_only) contours_drop_parent = return_parent_contours(contours_drop, hir_on_drop) - areas_cnt_text = np.array([cv2.contourArea(contours_drop_parent[j]) for j in range(len(contours_drop_parent))]) + areas_cnt_text = np.array([cv2.contourArea(contours_drop_parent[j]) + for j in range(len(contours_drop_parent))]) areas_cnt_text = areas_cnt_text / float(drop_only.shape[0] * drop_only.shape[1]) - - contours_drop_parent = [contours_drop_parent[jz] for jz in range(len(contours_drop_parent)) if areas_cnt_text[jz] > 0.00001] - - areas_cnt_text = [areas_cnt_text[jz] for jz in range(len(areas_cnt_text)) if areas_cnt_text[jz] > 0.00001] + contours_drop_parent = [contours_drop_parent[jz] + for jz in range(len(contours_drop_parent)) + if areas_cnt_text[jz] > 0.00001] + areas_cnt_text = [areas_cnt_text[jz] + for jz in range(len(areas_cnt_text)) + if areas_cnt_text[jz] > 0.00001] contours_drop_parent_final = [] - for jj in range(len(contours_drop_parent)): x, y, w, h = cv2.boundingRect(contours_drop_parent[jj]) + box = slice(y, y + h), slice(x, x + w) + box0 = box + (0,) mask_of_drop_cpaital_in_early_layout = np.zeros((text_regions_p.shape[0], text_regions_p.shape[1])) + mask_of_drop_cpaital_in_early_layout[box] = text_regions_p[box] - mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w] = text_regions_p[y : y + h, x : x + w] - - all_drop_capital_pixels_which_is_text_in_early_lo = np.sum( mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w]==1 ) - - mask_of_drop_cpaital_in_early_layout[y : y + h, x : x + w]=1 - all_drop_capital_pixels = np.sum(mask_of_drop_cpaital_in_early_layout==1 ) + all_drop_capital_pixels_which_is_text_in_early_lo = np.sum(mask_of_drop_cpaital_in_early_layout[box]==1) + mask_of_drop_cpaital_in_early_layout[box] = 1 + all_drop_capital_pixels = np.sum(mask_of_drop_cpaital_in_early_layout==1) percent_text_to_all_in_drop = all_drop_capital_pixels_which_is_text_in_early_lo / float(all_drop_capital_pixels) - - - if ( ( areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) ) / float(w*h) ) > 0.6 and percent_text_to_all_in_drop>=0.3: - - layout_in_patch[y : y + h, x : x + w, 0] = drop_capital_label + if (areas_cnt_text[jj] * float(drop_only.shape[0] * drop_only.shape[1]) / float(w * h) > 0.6 and + percent_text_to_all_in_drop >= 0.3): + layout_in_patch[box0] = drop_capital_label else: - layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = drop_capital_label - layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == 0] = drop_capital_label - layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == 4] = drop_capital_label# images - #layout_in_patch[y : y + h, x : x + w, 0][layout_in_patch[y : y + h, x : x + w, 0] == drop_capital_label] = 1#drop_capital_label + layout_in_patch[box0][layout_in_patch[box0] == drop_capital_label] = drop_capital_label + layout_in_patch[box0][layout_in_patch[box0] == 0] = drop_capital_label + layout_in_patch[box0][layout_in_patch[box0] == 4] = drop_capital_label# images + #layout_in_patch[box0][layout_in_patch[box0] == drop_capital_label] = 1#drop_capital_label return layout_in_patch -def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_textline_polygons,slopes,contours_only_text_parent_d_ordered): - - cx_main,cy_main ,x_min_main , x_max_main, y_min_main ,y_max_main,y_corr_x_min_from_argmin=find_new_features_of_contours(contours_only_text_parent) +def check_any_text_region_in_model_one_is_main_or_header( + regions_model_1, regions_model_full, + contours_only_text_parent, + all_box_coord, all_found_textline_polygons, + slopes, + contours_only_text_parent_d_ordered): + + cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin = \ + find_new_features_of_contours(contours_only_text_parent) length_con=x_max_main-x_min_main height_con=y_max_main-y_min_main - - all_found_textline_polygons_main=[] all_found_textline_polygons_head=[] @@ -843,14 +891,10 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions img=np.zeros((regions_model_1.shape[0],regions_model_1.shape[1],3)) img = cv2.fillPoly(img, pts=[con], color=(255, 255, 255)) - - all_pixels=((img[:,:,0]==255)*1).sum() - pixels_header=( ( (img[:,:,0]==255) & (regions_model_full[:,:,0]==2) )*1 ).sum() pixels_main=all_pixels-pixels_header - if (pixels_header>=pixels_main) and ( (length_con[ii]/float(height_con[ii]) )>=1.3 ): regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=2 contours_only_text_parent_head.append(con) @@ -870,28 +914,44 @@ def check_any_text_region_in_model_one_is_main_or_header(regions_model_1,regions #print(all_pixels,pixels_main,pixels_header) - return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_textline_polygons_main,all_found_textline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d + return (regions_model_1, + contours_only_text_parent_main, + contours_only_text_parent_head, + all_box_coord_main, + all_box_coord_head, + all_found_textline_polygons_main, + all_found_textline_polygons_head, + slopes_main, + slopes_head, + contours_only_text_parent_main_d, + contours_only_text_parent_head_d) +def check_any_text_region_in_model_one_is_main_or_header_light( + regions_model_1, regions_model_full, + contours_only_text_parent, + all_box_coord, all_found_textline_polygons, + slopes, + contours_only_text_parent_d_ordered): -def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,regions_model_full,contours_only_text_parent,all_box_coord,all_found_textline_polygons,slopes,contours_only_text_parent_d_ordered): - ### to make it faster h_o = regions_model_1.shape[0] w_o = regions_model_1.shape[1] - - regions_model_1 = cv2.resize(regions_model_1, (int(regions_model_1.shape[1]/3.), int(regions_model_1.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) - regions_model_full = cv2.resize(regions_model_full, (int(regions_model_full.shape[1]/3.), int(regions_model_full.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) - contours_only_text_parent = [ (i/3.).astype(np.int32) for i in contours_only_text_parent] + zoom = 3 + regions_model_1 = cv2.resize(regions_model_1, (regions_model_1.shape[1] // zoom, + regions_model_1.shape[0] // zoom), + interpolation=cv2.INTER_NEAREST) + regions_model_full = cv2.resize(regions_model_full, (regions_model_full.shape[1] // zoom, + regions_model_full.shape[0] // zoom), + interpolation=cv2.INTER_NEAREST) + contours_only_text_parent = [(i / zoom).astype(int) for i in contours_only_text_parent] ### - - cx_main,cy_main ,x_min_main , x_max_main, y_min_main ,y_max_main,y_corr_x_min_from_argmin=find_new_features_of_contours(contours_only_text_parent) + cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin = \ + find_new_features_of_contours(contours_only_text_parent) length_con=x_max_main-x_min_main height_con=y_max_main-y_min_main - - all_found_textline_polygons_main=[] all_found_textline_polygons_head=[] @@ -909,16 +969,13 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r for ii in range(len(contours_only_text_parent)): con=contours_only_text_parent[ii] - img=np.zeros((regions_model_1.shape[0],regions_model_1.shape[1],3)) + img=np.zeros((regions_model_1.shape[0], regions_model_1.shape[1], 3)) img = cv2.fillPoly(img, pts=[con], color=(255, 255, 255)) - - - all_pixels=((img[:,:,0]==255)*1).sum() - - pixels_header=( ( (img[:,:,0]==255) & (regions_model_full[:,:,0]==2) )*1 ).sum() - pixels_main=all_pixels-pixels_header - + all_pixels = (img[:,:,0]==255).sum() + pixels_header=((img[:,:,0]==255) & + (regions_model_full[:,:,0]==2)).sum() + pixels_main = all_pixels - pixels_header if (pixels_header>=pixels_main) and ( (length_con[ii]/float(height_con[ii]) )>=1.3 ): regions_model_1[:,:][(regions_model_1[:,:]==1) & (img[:,:,0]==255) ]=2 @@ -939,22 +996,30 @@ def check_any_text_region_in_model_one_is_main_or_header_light(regions_model_1,r #print(all_pixels,pixels_main,pixels_header) - - ### to make it faster - regions_model_1 = cv2.resize(regions_model_1, (w_o, h_o), interpolation=cv2.INTER_NEAREST) - #regions_model_full = cv2.resize(img, (int(regions_model_full.shape[1]/3.), int(regions_model_full.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) - contours_only_text_parent_head = [ (i*3.).astype(np.int32) for i in contours_only_text_parent_head] - contours_only_text_parent_main = [ (i*3.).astype(np.int32) for i in contours_only_text_parent_main] + # regions_model_full = cv2.resize(img, (regions_model_full.shape[1] // zoom, + # regions_model_full.shape[0] // zoom), + # interpolation=cv2.INTER_NEAREST) + contours_only_text_parent_head = [(i * zoom).astype(int) for i in contours_only_text_parent_head] + contours_only_text_parent_main = [(i * zoom).astype(int) for i in contours_only_text_parent_main] ### - return regions_model_1,contours_only_text_parent_main,contours_only_text_parent_head,all_box_coord_main,all_box_coord_head,all_found_textline_polygons_main,all_found_textline_polygons_head,slopes_main,slopes_head,contours_only_text_parent_main_d,contours_only_text_parent_head_d + return (regions_model_1, + contours_only_text_parent_main, + contours_only_text_parent_head, + all_box_coord_main, + all_box_coord_head, + all_found_textline_polygons_main, + all_found_textline_polygons_head, + slopes_main, + slopes_head, + contours_only_text_parent_main_d, + contours_only_text_parent_head_d) def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col): # print(textlines_con) # textlines_con=textlines_con.astype(np.uint32) - textlines_con_changed = [] for m1 in range(len(textlines_con)): @@ -973,9 +1038,10 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col) ##plt.imshow(img_text_all) ##plt.show() - areas_cnt_text = np.array([cv2.contourArea(textlines_tot[j]) for j in range(len(textlines_tot))]) + areas_cnt_text = np.array([cv2.contourArea(textlines_tot[j]) + for j in range(len(textlines_tot))]) areas_cnt_text = areas_cnt_text / float(textline_iamge.shape[0] * textline_iamge.shape[1]) - indexes_textlines = np.array(range(len(textlines_tot))) + indexes_textlines = np.arange(len(textlines_tot)) # print(areas_cnt_text,np.min(areas_cnt_text),np.max(areas_cnt_text)) if num_col == 0: @@ -1010,9 +1076,7 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col) sum_small_big_all2 = (sum_small_big_all[:, :] == 2) * 1 sum_intersection_sb = sum_small_big_all2.sum(axis=1).sum() - if sum_intersection_sb > 0: - dis_small_from_bigs_tot = [] for z1 in range(len(textlines_small)): # print(len(textlines_small),'small') @@ -1028,27 +1092,22 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col) sum_small_big_2 = (sum_small_big[:, :] == 2) * 1 sum_intersection = sum_small_big_2.sum(axis=1).sum() - # print(sum_intersection) - intersections.append(sum_intersection) if len(np.array(intersections)[np.array(intersections) > 0]) == 0: intersections = [] - try: dis_small_from_bigs_tot.append(np.argmax(intersections)) except: dis_small_from_bigs_tot.append(-1) smalls_list = np.array(dis_small_from_bigs_tot)[np.array(dis_small_from_bigs_tot) >= 0] - # index_small_textlines_rest=list( set(indexes_textlines_small)-set(smalls_list) ) textlines_big_with_change = [] textlines_big_with_change_con = [] textlines_small_with_change = [] - for z in list(set(smalls_list)): index_small_textlines = list(np.where(np.array(dis_small_from_bigs_tot) == z)[0]) # print(z,index_small_textlines) @@ -1068,7 +1127,6 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col) cont, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # print(cont[0],type(cont)) - textlines_big_with_change_con.append(cont) textlines_big_org_form[z] = cont[0] @@ -1079,13 +1137,11 @@ def small_textlines_to_parent_adherence2(textlines_con, textline_iamge, num_col) # print(textlines_small_with_change,'textlines_small_with_change') # print(textlines_big) textlines_con_changed.append(textlines_big_org_form) - else: textlines_con_changed.append(textlines_big_org_form) return textlines_con_changed def order_of_regions(textline_mask, contours_main, contours_header, y_ref): - ##plt.imshow(textline_mask) ##plt.show() """ @@ -1095,59 +1151,47 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): y_help=np.zeros(len(y)+40) y_help[20:len(y)+20]=y - x=np.array( range(len(y)) ) - + x=np.arange(len(y)) peaks_real, _ = find_peaks(gaussian_filter1d(y, 3), height=0) - ##plt.imshow(textline_mask[:,:]) ##plt.show() - sigma_gaus=8 - z= gaussian_filter1d(y_help, sigma_gaus) zneg_rev=-y_help+np.max(y_help) - zneg=np.zeros(len(zneg_rev)+40) zneg[20:len(zneg_rev)+20]=zneg_rev zneg= gaussian_filter1d(zneg, sigma_gaus) - peaks, _ = find_peaks(z, height=0) peaks_neg, _ = find_peaks(zneg, height=0) - peaks_neg=peaks_neg-20-20 peaks=peaks-20 """ - textline_sum_along_width = textline_mask.sum(axis=1) y = textline_sum_along_width[:] y_padded = np.zeros(len(y) + 40) y_padded[20 : len(y) + 20] = y - x = np.array(range(len(y))) + x = np.arange(len(y)) peaks_real, _ = find_peaks(gaussian_filter1d(y, 3), height=0) sigma_gaus = 8 - z = gaussian_filter1d(y_padded, sigma_gaus) zneg_rev = -y_padded + np.max(y_padded) - zneg = np.zeros(len(zneg_rev) + 40) zneg[20 : len(zneg_rev) + 20] = zneg_rev zneg = gaussian_filter1d(zneg, sigma_gaus) peaks, _ = find_peaks(z, height=0) peaks_neg, _ = find_peaks(zneg, height=0) - peaks_neg = peaks_neg - 20 - 20 peaks = peaks - 20 ##plt.plot(z) ##plt.show() - if contours_main != None: areas_main = np.array([cv2.contourArea(contours_main[j]) for j in range(len(contours_main))]) M_main = [cv2.moments(contours_main[j]) for j in range(len(contours_main))] @@ -1173,42 +1217,32 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): # print(cy_main,'mainy') peaks_neg_new = [] - peaks_neg_new.append(0 + y_ref) for iii in range(len(peaks_neg)): peaks_neg_new.append(peaks_neg[iii] + y_ref) - peaks_neg_new.append(textline_mask.shape[0] + y_ref) if len(cy_main) > 0 and np.max(cy_main) > np.max(peaks_neg_new): cy_main = np.array(cy_main) * (np.max(peaks_neg_new) / np.max(cy_main)) - 10 - if contours_main != None: - indexer_main = np.array(range(len(contours_main))) - + indexer_main = np.arange(len(contours_main)) if contours_main != None: len_main = len(contours_main) else: len_main = 0 matrix_of_orders = np.zeros((len(contours_main) + len(contours_header), 5)) - - matrix_of_orders[:, 0] = np.array(range(len(contours_main) + len(contours_header))) - + matrix_of_orders[:, 0] = np.arange(len(contours_main) + len(contours_header)) matrix_of_orders[: len(contours_main), 1] = 1 matrix_of_orders[len(contours_main) :, 1] = 2 - matrix_of_orders[: len(contours_main), 2] = cx_main matrix_of_orders[len(contours_main) :, 2] = cx_header - matrix_of_orders[: len(contours_main), 3] = cy_main matrix_of_orders[len(contours_main) :, 3] = cy_header - - matrix_of_orders[: len(contours_main), 4] = np.array(range(len(contours_main))) - matrix_of_orders[len(contours_main) :, 4] = np.array(range(len(contours_header))) + matrix_of_orders[: len(contours_main), 4] = np.arange(len(contours_main)) + matrix_of_orders[len(contours_main) :, 4] = np.arange(len(contours_header)) # print(peaks_neg_new,'peaks_neg_new') - # print(matrix_of_orders,'matrix_of_orders') # print(peaks_neg_new,np.max(peaks_neg_new)) final_indexers_sorted = [] @@ -1217,19 +1251,20 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): for i in range(len(peaks_neg_new) - 1): top = peaks_neg_new[i] down = peaks_neg_new[i + 1] - - indexes_in = matrix_of_orders[:, 0][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - cxs_in = matrix_of_orders[:, 2][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - cys_in = matrix_of_orders[:, 3][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - types_of_text = matrix_of_orders[:, 1][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - index_types_of_text = matrix_of_orders[:, 4][(matrix_of_orders[:, 3] >= top) & ((matrix_of_orders[:, 3] < down))] - + indexes_in = matrix_of_orders[:, 0][(matrix_of_orders[:, 3] >= top) & + ((matrix_of_orders[:, 3] < down))] + cxs_in = matrix_of_orders[:, 2][(matrix_of_orders[:, 3] >= top) & + ((matrix_of_orders[:, 3] < down))] + cys_in = matrix_of_orders[:, 3][(matrix_of_orders[:, 3] >= top) & + ((matrix_of_orders[:, 3] < down))] + types_of_text = matrix_of_orders[:, 1][(matrix_of_orders[:, 3] >= top) & + (matrix_of_orders[:, 3] < down)] + index_types_of_text = matrix_of_orders[:, 4][(matrix_of_orders[:, 3] >= top) & + (matrix_of_orders[:, 3] < down)] sorted_inside = np.argsort(cxs_in) - ind_in_int = indexes_in[sorted_inside] ind_in_type = types_of_text[sorted_inside] ind_ind_type = index_types_of_text[sorted_inside] - for j in range(len(ind_in_int)): final_indexers_sorted.append(int(ind_in_int[j])) final_types.append(int(ind_in_type[j])) @@ -1237,20 +1272,22 @@ def order_of_regions(textline_mask, contours_main, contours_header, y_ref): ##matrix_of_orders[:len_main,4]=final_indexers_sorted[:] - # This fix is applied if the sum of the lengths of contours and contours_h does not match final_indexers_sorted. However, this is not the optimal solution.. - if (len(cy_main)+len(cy_header) ) == len(final_index_type): + # This fix is applied if the sum of the lengths of contours and contours_h + # does not match final_indexers_sorted. However, this is not the optimal solution.. + if len(cy_main) + len(cy_header) == len(final_index_type): pass else: - indexes_missed = set(list( np.array( range((len(cy_main)+len(cy_header) ) )) )) - set(final_indexers_sorted) + indexes_missed = set(np.arange(len(cy_main) + len(cy_header))) - set(final_indexers_sorted) for ind_missed in indexes_missed: final_indexers_sorted.append(ind_missed) final_types.append(1) final_index_type.append(ind_missed) - - + return final_indexers_sorted, matrix_of_orders, final_types, final_index_type -def combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(img_p_in_ver, img_in_hor,num_col_classifier): +def combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new( + img_p_in_ver, img_in_hor,num_col_classifier): + #img_p_in_ver = cv2.erode(img_p_in_ver, self.kernel, iterations=2) img_p_in_ver=img_p_in_ver.astype(np.uint8) img_p_in_ver=np.repeat(img_p_in_ver[:, :, np.newaxis], 3, axis=2) @@ -1258,33 +1295,33 @@ def combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(im ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_lines_ver,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - - slope_lines_ver,dist_x_ver, x_min_main_ver ,x_max_main_ver ,cy_main_ver,slope_lines_org_ver,y_min_main_ver, y_max_main_ver, cx_main_ver=find_features_of_lines(contours_lines_ver) - + slope_lines_ver, _, x_min_main_ver, _, _, _, y_min_main_ver, y_max_main_ver, cx_main_ver = \ + find_features_of_lines(contours_lines_ver) for i in range(len(x_min_main_ver)): - img_p_in_ver[int(y_min_main_ver[i]):int(y_min_main_ver[i])+30,int(cx_main_ver[i])-25:int(cx_main_ver[i])+25,0]=0 - img_p_in_ver[int(y_max_main_ver[i])-30:int(y_max_main_ver[i]),int(cx_main_ver[i])-25:int(cx_main_ver[i])+25,0]=0 - - + img_p_in_ver[int(y_min_main_ver[i]): + int(y_min_main_ver[i])+30, + int(cx_main_ver[i])-25: + int(cx_main_ver[i])+25, 0] = 0 + img_p_in_ver[int(y_max_main_ver[i])-30: + int(y_max_main_ver[i]), + int(cx_main_ver[i])-25: + int(cx_main_ver[i])+25, 0] = 0 + img_in_hor=img_in_hor.astype(np.uint8) img_in_hor=np.repeat(img_in_hor[:, :, np.newaxis], 3, axis=2) imgray = cv2.cvtColor(img_in_hor, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_lines_hor,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - slope_lines_hor,dist_x_hor, x_min_main_hor ,x_max_main_hor ,cy_main_hor,slope_lines_org_hor,y_min_main_hor, y_max_main_hor, cx_main_hor=find_features_of_lines(contours_lines_hor) - - + slope_lines_hor, dist_x_hor, x_min_main_hor, x_max_main_hor, cy_main_hor, _, _, _, _ = \ + find_features_of_lines(contours_lines_hor) x_width_smaller_than_acolumn_width=img_in_hor.shape[1]/float(num_col_classifier+1.) len_lines_bigger_than_x_width_smaller_than_acolumn_width=len( dist_x_hor[dist_x_hor>=x_width_smaller_than_acolumn_width] ) - - len_lines_bigger_than_x_width_smaller_than_acolumn_width_per_column=int( len_lines_bigger_than_x_width_smaller_than_acolumn_width/float(num_col_classifier) ) - - - if len_lines_bigger_than_x_width_smaller_than_acolumn_width_per_column<10: - args_hor=np.array( range(len(slope_lines_hor) )) + len_lines_bigger_than_x_width_smaller_than_acolumn_width_per_column=int(len_lines_bigger_than_x_width_smaller_than_acolumn_width / + float(num_col_classifier)) + if len_lines_bigger_than_x_width_smaller_than_acolumn_width_per_column < 10: + args_hor=np.arange(len(slope_lines_hor)) all_args_uniq=contours_in_same_horizon(cy_main_hor) #print(all_args_uniq,'all_args_uniq') if len(all_args_uniq)>0: @@ -1302,51 +1339,50 @@ def combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(im #print(img_p_in_ver.shape[1],some_x_max-some_x_min,'xdiff') diff_x_some=some_x_max-some_x_min for jv in range(len(some_args)): - - img_p_in=cv2.fillPoly(img_in_hor, pts =[contours_lines_hor[some_args[jv]]], color=(1,1,1)) - + img_p_in=cv2.fillPoly(img_in_hor, pts=[contours_lines_hor[some_args[jv]]], color=(1,1,1)) if any(i_diff>(img_p_in_ver.shape[1]/float(3.3)) for i_diff in diff_x_some): - img_p_in[int(np.mean(some_cy))-5:int(np.mean(some_cy))+5, int(np.min(some_x_min)):int(np.max(some_x_max)) ]=1 - + img_p_in[int(np.mean(some_cy))-5: + int(np.mean(some_cy))+5, + int(np.min(some_x_min)): + int(np.max(some_x_max)) ]=1 sum_dis=dist_x_hor[some_args].sum() diff_max_min_uniques=np.max(x_max_main_hor[some_args])-np.min(x_min_main_hor[some_args]) - - if diff_max_min_uniques>sum_dis and ( (sum_dis/float(diff_max_min_uniques) ) >0.85 ) and ( (diff_max_min_uniques/float(img_p_in_ver.shape[1]))>0.85 ) and np.std( dist_x_hor[some_args] )<(0.55*np.mean( dist_x_hor[some_args] )): - #print(dist_x_hor[some_args],dist_x_hor[some_args].sum(),np.min(x_min_main_hor[some_args]) ,np.max(x_max_main_hor[some_args]),'jalibdi') - #print(np.mean( dist_x_hor[some_args] ),np.std( dist_x_hor[some_args] ),np.var( dist_x_hor[some_args] ),'jalibdiha') + if (diff_max_min_uniques > sum_dis and + sum_dis / float(diff_max_min_uniques) > 0.85 and + diff_max_min_uniques / float(img_p_in_ver.shape[1]) > 0.85 and + np.std(dist_x_hor[some_args]) < 0.55 * np.mean(dist_x_hor[some_args])): + # print(dist_x_hor[some_args], + # dist_x_hor[some_args].sum(), + # np.min(x_min_main_hor[some_args]), + # np.max(x_max_main_hor[some_args]),'jalibdi') + # print(np.mean( dist_x_hor[some_args] ), + # np.std( dist_x_hor[some_args] ), + # np.var( dist_x_hor[some_args] ),'jalibdiha') special_separators.append(np.mean(cy_main_hor[some_args])) - else: img_p_in=img_in_hor special_separators=[] else: img_p_in=img_in_hor special_separators=[] - img_p_in_ver[:,:,0][img_p_in_ver[:,:,0]==255]=1 sep_ver_hor=img_p_in+img_p_in_ver - - sep_ver_hor_cross=(sep_ver_hor[:,:,0]==2)*1 - sep_ver_hor_cross=np.repeat(sep_ver_hor_cross[:, :, np.newaxis], 3, axis=2) sep_ver_hor_cross=sep_ver_hor_cross.astype(np.uint8) imgray = cv2.cvtColor(sep_ver_hor_cross, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_cross,_=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - cx_cross,cy_cross ,_ , _, _ ,_,_=find_new_features_of_contours(contours_cross) - for ii in range(len(cx_cross)): img_p_in[int(cy_cross[ii])-30:int(cy_cross[ii])+30,int(cx_cross[ii])+5:int(cx_cross[ii])+40,0]=0 img_p_in[int(cy_cross[ii])-30:int(cy_cross[ii])+30,int(cx_cross[ii])-40:int(cx_cross[ii])-4,0]=0 - else: img_p_in=np.copy(img_in_hor) special_separators=[] - return img_p_in[:,:,0],special_separators + return img_p_in[:,:,0], special_separators def return_points_with_boundies(peaks_neg_fin, first_point, last_point): peaks_neg_tot = [] @@ -1359,62 +1395,49 @@ def return_points_with_boundies(peaks_neg_fin, first_point, last_point): def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, pixel_lines, contours_h=None): t_ins_c0 = time.time() separators_closeup=( (region_pre_p[:,:,:]==pixel_lines))*1 - separators_closeup[0:110,:,:]=0 separators_closeup[separators_closeup.shape[0]-150:,:,:]=0 kernel = np.ones((5,5),np.uint8) - separators_closeup=separators_closeup.astype(np.uint8) separators_closeup = cv2.dilate(separators_closeup,kernel,iterations = 1) separators_closeup = cv2.erode(separators_closeup,kernel,iterations = 1) - separators_closeup_new=np.zeros((separators_closeup.shape[0] ,separators_closeup.shape[1] )) separators_closeup_n=np.copy(separators_closeup) - separators_closeup_n=separators_closeup_n.astype(np.uint8) - + separators_closeup_n_binary=np.zeros(( separators_closeup_n.shape[0],separators_closeup_n.shape[1]) ) separators_closeup_n_binary[:,:]=separators_closeup_n[:,:,0] - separators_closeup_n_binary[:,:][separators_closeup_n_binary[:,:]!=0]=1 gray_early=np.repeat(separators_closeup_n_binary[:, :, np.newaxis], 3, axis=2) gray_early=gray_early.astype(np.uint8) - imgray_e = cv2.cvtColor(gray_early, cv2.COLOR_BGR2GRAY) ret_e, thresh_e = cv2.threshold(imgray_e, 0, 255, 0) contours_line_e,hierarchy_e=cv2.findContours(thresh_e,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - - slope_linese,dist_xe, x_min_maine ,x_max_maine ,cy_maine,slope_lines_orge,y_min_maine, y_max_maine, cx_maine=find_features_of_lines(contours_line_e) - - dist_ye=y_max_maine-y_min_maine - - - args_e=np.array(range(len(contours_line_e))) - args_hor_e=args_e[(dist_ye<=50) & (dist_xe>=3*dist_ye)] - - + _, dist_xe, _, _, _, _, y_min_main, y_max_main, _ = \ + find_features_of_lines(contours_line_e) + dist_ye = y_max_main - y_min_main + args_e=np.arange(len(contours_line_e)) + args_hor_e=args_e[(dist_ye<=50) & + (dist_xe>=3*dist_ye)] cnts_hor_e=[] for ce in args_hor_e: cnts_hor_e.append(contours_line_e[ce]) - figs_e=np.zeros(thresh_e.shape) figs_e=cv2.fillPoly(figs_e,pts=cnts_hor_e,color=(1,1,1)) - separators_closeup_n_binary=cv2.fillPoly(separators_closeup_n_binary,pts=cnts_hor_e,color=(0,0,0)) - + separators_closeup_n_binary=cv2.fillPoly(separators_closeup_n_binary, pts=cnts_hor_e, color=(0,0,0)) gray = cv2.bitwise_not(separators_closeup_n_binary) gray=gray.astype(np.uint8) bw = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, \ - cv2.THRESH_BINARY, 15, -2) - + cv2.THRESH_BINARY, 15, -2) horizontal = np.copy(bw) vertical = np.copy(bw) - + cols = horizontal.shape[1] horizontal_size = cols // 30 # Create structure element for extracting horizontal lines through morphology operations @@ -1424,12 +1447,9 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, horizontal = cv2.dilate(horizontal, horizontalStructure) kernel = np.ones((5,5),np.uint8) - - horizontal = cv2.dilate(horizontal,kernel,iterations = 2) horizontal = cv2.erode(horizontal,kernel,iterations = 2) - - horizontal=cv2.fillPoly(horizontal,pts=cnts_hor_e,color=(255,255,255)) + horizontal = cv2.fillPoly(horizontal, pts=cnts_hor_e, color=(255,255,255)) rows = vertical.shape[0] verticalsize = rows // 30 @@ -1438,10 +1458,11 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, # Apply morphology operations vertical = cv2.erode(vertical, verticalStructure) vertical = cv2.dilate(vertical, verticalStructure) - vertical = cv2.dilate(vertical,kernel,iterations = 1) - horizontal,special_separators=combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new(vertical,horizontal,num_col_classifier) + horizontal, special_separators = \ + combine_hor_lines_and_delete_cross_points_and_get_lines_features_back_new( + vertical, horizontal, num_col_classifier) separators_closeup_new[:,:][vertical[:,:]!=0]=1 separators_closeup_new[:,:][horizontal[:,:]!=0]=1 @@ -1453,9 +1474,10 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_line_vers,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - slope_lines,dist_x, x_min_main ,x_max_main ,cy_main,slope_lines_org,y_min_main, y_max_main, cx_main=find_features_of_lines(contours_line_vers) + slope_lines, dist_x, x_min_main, x_max_main, cy_main, slope_lines_org, y_min_main, y_max_main, cx_main = \ + find_features_of_lines(contours_line_vers) - args=np.array( range(len(slope_lines) )) + args=np.arange(len(slope_lines)) args_ver=args[slope_lines==1] dist_x_ver=dist_x[slope_lines==1] y_min_main_ver=y_min_main[slope_lines==1] @@ -1466,19 +1488,17 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, dist_y_ver=y_max_main_ver-y_min_main_ver len_y=separators_closeup.shape[0]/3.0 - horizontal=np.repeat(horizontal[:, :, np.newaxis], 3, axis=2) horizontal=horizontal.astype(np.uint8) imgray = cv2.cvtColor(horizontal, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) - contours_line_hors,hierarchy=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) - slope_lines,dist_x, x_min_main ,x_max_main ,cy_main,slope_lines_org,y_min_main, y_max_main, cx_main=find_features_of_lines(contours_line_hors) + slope_lines, dist_x, x_min_main, x_max_main, cy_main, slope_lines_org, y_min_main, y_max_main, cx_main = \ + find_features_of_lines(contours_line_hors) slope_lines_org_hor=slope_lines_org[slope_lines==0] - args=np.array( range(len(slope_lines) )) + args=np.arange(len(slope_lines)) len_x=separators_closeup.shape[1]/5.0 - dist_y=np.abs(y_max_main-y_min_main) args_hor=args[slope_lines==0] @@ -1497,109 +1517,84 @@ def find_number_of_columns_in_document(region_pre_p, num_col_classifier, tables, y_min_main_hor=y_min_main_hor[dist_x_hor>=len_x/2.0] y_max_main_hor=y_max_main_hor[dist_x_hor>=len_x/2.0] dist_y_hor=dist_y_hor[dist_x_hor>=len_x/2.0] - slope_lines_org_hor=slope_lines_org_hor[dist_x_hor>=len_x/2.0] dist_x_hor=dist_x_hor[dist_x_hor>=len_x/2.0] - matrix_of_lines_ch=np.zeros((len(cy_main_hor)+len(cx_main_ver),10)) - matrix_of_lines_ch[:len(cy_main_hor),0]=args_hor matrix_of_lines_ch[len(cy_main_hor):,0]=args_ver - - matrix_of_lines_ch[len(cy_main_hor):,1]=cx_main_ver - matrix_of_lines_ch[:len(cy_main_hor),2]=x_min_main_hor+50#x_min_main_hor+150 matrix_of_lines_ch[len(cy_main_hor):,2]=x_min_main_ver - matrix_of_lines_ch[:len(cy_main_hor),3]=x_max_main_hor-50#x_max_main_hor-150 matrix_of_lines_ch[len(cy_main_hor):,3]=x_max_main_ver - matrix_of_lines_ch[:len(cy_main_hor),4]=dist_x_hor matrix_of_lines_ch[len(cy_main_hor):,4]=dist_x_ver - matrix_of_lines_ch[:len(cy_main_hor),5]=cy_main_hor - - matrix_of_lines_ch[:len(cy_main_hor),6]=y_min_main_hor matrix_of_lines_ch[len(cy_main_hor):,6]=y_min_main_ver - matrix_of_lines_ch[:len(cy_main_hor),7]=y_max_main_hor matrix_of_lines_ch[len(cy_main_hor):,7]=y_max_main_ver - matrix_of_lines_ch[:len(cy_main_hor),8]=dist_y_hor matrix_of_lines_ch[len(cy_main_hor):,8]=dist_y_ver - - matrix_of_lines_ch[len(cy_main_hor):,9]=1 if contours_h is not None: - slope_lines_head,dist_x_head, x_min_main_head ,x_max_main_head ,cy_main_head,slope_lines_org_head,y_min_main_head, y_max_main_head, cx_main_head=find_features_of_lines(contours_h) + _, dist_x_head, x_min_main_head, x_max_main_head, cy_main_head, _, y_min_main_head, y_max_main_head, _ = \ + find_features_of_lines(contours_h) matrix_l_n=np.zeros((matrix_of_lines_ch.shape[0]+len(cy_main_head),matrix_of_lines_ch.shape[1])) matrix_l_n[:matrix_of_lines_ch.shape[0],:]=np.copy(matrix_of_lines_ch[:,:]) - args_head=np.array(range(len(cy_main_head)))+len(cy_main_hor) + args_head=np.arange(len(cy_main_head)) + len(cy_main_hor) matrix_l_n[matrix_of_lines_ch.shape[0]:,0]=args_head matrix_l_n[matrix_of_lines_ch.shape[0]:,2]=x_min_main_head+30 matrix_l_n[matrix_of_lines_ch.shape[0]:,3]=x_max_main_head-30 - matrix_l_n[matrix_of_lines_ch.shape[0]:,4]=dist_x_head - matrix_l_n[matrix_of_lines_ch.shape[0]:,5]=y_min_main_head-3-8 matrix_l_n[matrix_of_lines_ch.shape[0]:,6]=y_min_main_head-5-8 matrix_l_n[matrix_of_lines_ch.shape[0]:,7]=y_max_main_head#y_min_main_head+1-8 matrix_l_n[matrix_of_lines_ch.shape[0]:,8]=4 - matrix_of_lines_ch=np.copy(matrix_l_n) - - - cy_main_splitters=cy_main_hor[ (x_min_main_hor<=.16*region_pre_p.shape[1]) & (x_max_main_hor>=.84*region_pre_p.shape[1] )] + cy_main_splitters=cy_main_hor[(x_min_main_hor<=.16*region_pre_p.shape[1]) & + (x_max_main_hor>=.84*region_pre_p.shape[1])] cy_main_splitters=np.array( list(cy_main_splitters)+list(special_separators)) - if contours_h is not None: try: - cy_main_splitters_head=cy_main_head[ (x_min_main_head<=.16*region_pre_p.shape[1]) & (x_max_main_head>=.84*region_pre_p.shape[1] )] + cy_main_splitters_head=cy_main_head[(x_min_main_head<=.16*region_pre_p.shape[1]) & + (x_max_main_head>=.84*region_pre_p.shape[1])] cy_main_splitters=np.array( list(cy_main_splitters)+list(cy_main_splitters_head)) except: pass args_cy_splitter=np.argsort(cy_main_splitters) - cy_main_splitters_sort=cy_main_splitters[args_cy_splitter] splitter_y_new=[] splitter_y_new.append(0) for i in range(len(cy_main_splitters_sort)): splitter_y_new.append( cy_main_splitters_sort[i] ) - splitter_y_new.append(region_pre_p.shape[0]) - splitter_y_new_diff=np.diff(splitter_y_new)/float(region_pre_p.shape[0])*100 - args_big_parts=np.array(range(len(splitter_y_new_diff))) [ splitter_y_new_diff>22 ] - + args_big_parts=np.arange(len(splitter_y_new_diff))[ splitter_y_new_diff>22 ] + regions_without_separators=return_regions_without_separators(region_pre_p) - - length_y_threshold=regions_without_separators.shape[0]/4.0 num_col_fin=0 peaks_neg_fin_fin=[] - for itiles in args_big_parts: - regions_without_separators_tile=regions_without_separators[int(splitter_y_new[itiles]):int(splitter_y_new[itiles+1]),:,0] - + regions_without_separators_tile=regions_without_separators[int(splitter_y_new[itiles]): + int(splitter_y_new[itiles+1]),:,0] try: - num_col, peaks_neg_fin = find_num_col(regions_without_separators_tile, num_col_classifier, tables, multiplier=7.0) + num_col, peaks_neg_fin = find_num_col(regions_without_separators_tile, + num_col_classifier, tables, multiplier=7.0) except: num_col = 0 peaks_neg_fin = [] - if num_col>num_col_fin: num_col_fin=num_col peaks_neg_fin_fin=peaks_neg_fin - if len(args_big_parts)==1 and (len(peaks_neg_fin_fin)+1) splitter_y_new[i] ) & (matrix_of_lines_ch[:,7]< splitter_y_new[i+1] ) ] + matrix_new = matrix_of_lines_ch[:,:][(matrix_of_lines_ch[:,6]> splitter_y_new[i] ) & + (matrix_of_lines_ch[:,7]< splitter_y_new[i+1] )] #print(len( matrix_new[:,9][matrix_new[:,9]==1] )) - #print(matrix_new[:,8][matrix_new[:,9]==1],'gaddaaa') - # check to see is there any vertical separator to find holes. - if 1>0:#len( matrix_new[:,9][matrix_new[:,9]==1] )>0 and np.max(matrix_new[:,8][matrix_new[:,9]==1])>=0.1*(np.abs(splitter_y_new[i+1]-splitter_y_new[i] )): - + #if (len(matrix_new[:,9][matrix_new[:,9]==1]) > 0 and + # np.max(matrix_new[:,8][matrix_new[:,9]==1]) >= + # 0.1 * (np.abs(splitter_y_new[i+1]-splitter_y_new[i]))): + if True: try: if erosion_hurts: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], num_col_classifier, tables, multiplier=6.) + num_col, peaks_neg_fin = find_num_col( + regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], + num_col_classifier, tables, multiplier=6.) else: - num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],num_col_classifier, tables, multiplier=7.) + num_col, peaks_neg_fin = find_num_col( + regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], + num_col_classifier, tables, multiplier=7.) except: peaks_neg_fin=[] num_col = 0 - - try: peaks_neg_fin_org=np.copy(peaks_neg_fin) if (len(peaks_neg_fin)+1)=len(peaks_neg_fin2): peaks_neg_fin=list(np.copy(peaks_neg_fin1)) else: peaks_neg_fin=list(np.copy(peaks_neg_fin2)) - - - peaks_neg_fin=list(np.array(peaks_neg_fin)+peaks_neg_fin_early[i_n]) if i_n!=(len(peaks_neg_fin_early)-2): @@ -1682,10 +1686,6 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho #print(peaks_neg_fin,'peaks_neg_fin') peaks_neg_fin_rev=peaks_neg_fin_rev+peaks_neg_fin - - - - if len(peaks_neg_fin_rev)>=len(peaks_neg_fin_org): peaks_neg_fin=list(np.sort(peaks_neg_fin_rev)) num_col=len(peaks_neg_fin) @@ -1696,7 +1696,9 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho #print(peaks_neg_fin,'peaks_neg_fin') except: pass - #num_col, peaks_neg_fin=find_num_col(regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:],multiplier=7.0) + #num_col, peaks_neg_fin = find_num_col( + # regions_without_separators[int(splitter_y_new[i]):int(splitter_y_new[i+1]),:], + # multiplier=7.0) x_min_hor_some=matrix_new[:,2][ (matrix_new[:,9]==0) ] x_max_hor_some=matrix_new[:,3][ (matrix_new[:,9]==0) ] cy_hor_some=matrix_new[:,5][ (matrix_new[:,9]==0) ] @@ -1706,197 +1708,160 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho if right2left_readingorder: x_max_hor_some_new = regions_without_separators.shape[1] - x_min_hor_some x_min_hor_some_new = regions_without_separators.shape[1] - x_max_hor_some - x_min_hor_some =list(np.copy(x_min_hor_some_new)) x_max_hor_some =list(np.copy(x_max_hor_some_new)) - - - - peaks_neg_tot=return_points_with_boundies(peaks_neg_fin,0, regions_without_separators[:,:].shape[1]) - peaks_neg_tot_tables.append(peaks_neg_tot) - reading_order_type,x_starting,x_ending,y_type_2,y_diff_type_2,y_lines_without_mother,x_start_without_mother,x_end_without_mother,there_is_sep_with_child,y_lines_with_child_without_mother,x_start_with_child_without_mother,x_end_with_child_without_mother,new_main_sep_y=return_x_start_end_mothers_childs_and_type_of_reading_order(x_min_hor_some,x_max_hor_some,cy_hor_some,peaks_neg_tot,cy_hor_diff) - + reading_order_type, x_starting, x_ending, y_type_2, y_diff_type_2, \ + y_lines_without_mother, x_start_without_mother, x_end_without_mother, there_is_sep_with_child, \ + y_lines_with_child_without_mother, x_start_with_child_without_mother, x_end_with_child_without_mother, \ + new_main_sep_y = return_x_start_end_mothers_childs_and_type_of_reading_order( + x_min_hor_some, x_max_hor_some, cy_hor_some, peaks_neg_tot, cy_hor_diff) + x_starting = np.array(x_starting) + x_ending = np.array(x_ending) + y_type_2 = np.array(y_type_2) + y_diff_type_2 = np.array(y_diff_type_2) - if (reading_order_type==1) or (reading_order_type==0 and (len(y_lines_without_mother)>=2 or there_is_sep_with_child==1)): - - + if ((reading_order_type==1) or + (reading_order_type==0 and + (len(y_lines_without_mother)>=2 or there_is_sep_with_child==1))): try: y_grenze=int(splitter_y_new[i])+300 - - - #check if there is a big separator in this y_mains_sep_ohne_grenzen - args_early_ys=np.array(range(len(y_type_2))) - + args_early_ys=np.arange(len(y_type_2)) #print(args_early_ys,'args_early_ys') #print(int(splitter_y_new[i]),int(splitter_y_new[i+1])) - - y_type_2_up=np.array(y_type_2)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - x_starting_up=np.array(x_starting)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - x_ending_up=np.array(x_ending)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - y_diff_type_2_up=np.array(y_diff_type_2)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - args_up=args_early_ys[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - - - - if len(y_type_2_up)>0: - y_main_separator_up=y_type_2_up[(x_starting_up==0) & (x_ending_up==(len(peaks_neg_tot)-1) )] - y_diff_main_separator_up=y_diff_type_2_up[(x_starting_up==0) & (x_ending_up==(len(peaks_neg_tot)-1) )] - args_main_to_deleted=args_up[(x_starting_up==0) & (x_ending_up==(len(peaks_neg_tot)-1) )] + + x_starting_up = x_starting[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + x_ending_up = x_ending[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + y_type_2_up = y_type_2[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + y_diff_type_2_up = y_diff_type_2[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + args_up = args_early_ys[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + if len(y_type_2_up) > 0: + y_main_separator_up = y_type_2_up [(x_starting_up==0) & + (x_ending_up==(len(peaks_neg_tot)-1) )] + y_diff_main_separator_up = y_diff_type_2_up[(x_starting_up==0) & + (x_ending_up==(len(peaks_neg_tot)-1) )] + args_main_to_deleted = args_up[(x_starting_up==0) & + (x_ending_up==(len(peaks_neg_tot)-1) )] #print(y_main_separator_up,y_diff_main_separator_up,args_main_to_deleted,'fffffjammmm') - - if len(y_diff_main_separator_up)>0: - args_to_be_kept=np.array( list( set(args_early_ys)-set(args_main_to_deleted) ) ) + if len(y_diff_main_separator_up) > 0: + args_to_be_kept = np.array(list( set(args_early_ys) - set(args_main_to_deleted) )) #print(args_to_be_kept,'args_to_be_kept') - boxes.append([0,peaks_neg_tot[len(peaks_neg_tot)-1],int(splitter_y_new[i]),int( np.max(y_diff_main_separator_up))]) + boxes.append([0, peaks_neg_tot[len(peaks_neg_tot)-1], + int(splitter_y_new[i]), int( np.max(y_diff_main_separator_up))]) splitter_y_new[i]=[ np.max(y_diff_main_separator_up) ][0] #print(splitter_y_new[i],'splitter_y_new[i]') - y_type_2=np.array(y_type_2)[args_to_be_kept] - x_starting=np.array(x_starting)[args_to_be_kept] - x_ending=np.array(x_ending)[args_to_be_kept] - y_diff_type_2=np.array(y_diff_type_2)[args_to_be_kept] + y_type_2 = y_type_2[args_to_be_kept] + x_starting = x_starting[args_to_be_kept] + x_ending = x_ending[args_to_be_kept] + y_diff_type_2 = y_diff_type_2[args_to_be_kept] #print('galdiha') y_grenze=int(splitter_y_new[i])+200 - - - args_early_ys2=np.array(range(len(y_type_2))) - y_type_2_up=np.array(y_type_2)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - x_starting_up=np.array(x_starting)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - x_ending_up=np.array(x_ending)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - y_diff_type_2_up=np.array(y_diff_type_2)[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - args_up2=args_early_ys2[( np.array(y_type_2)>int(splitter_y_new[i]) ) & (np.array(y_type_2)<=y_grenze)] - - + args_early_ys2=np.arange(len(y_type_2)) + y_type_2_up=y_type_2[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + x_starting_up=x_starting[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + x_ending_up=x_ending[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + y_diff_type_2_up=y_diff_type_2[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] + args_up2=args_early_ys2[(y_type_2 > int(splitter_y_new[i])) & + (y_type_2 <= y_grenze)] #print(y_type_2_up,x_starting_up,x_ending_up,'didid') - - nodes_in=[] + nodes_in = [] for ij in range(len(x_starting_up)): - nodes_in=nodes_in+list(np.array(range(x_starting_up[ij],x_ending_up[ij]))) - - #print(np.unique(nodes_in),'nodes_in') + nodes_in = nodes_in + list(range(x_starting_up[ij], + x_ending_up[ij])) + nodes_in = np.unique(nodes_in) + #print(nodes_in,'nodes_in') - if set(np.unique(nodes_in))==set(np.array(range(len(peaks_neg_tot)-1)) ): + if set(nodes_in)==set(range(len(peaks_neg_tot)-1)): pass - elif set( np.unique(nodes_in) )==set( np.array(range(1,len(peaks_neg_tot)-1)) ): + elif set(nodes_in)==set(range(1, len(peaks_neg_tot)-1)): pass else: #print('burdaydikh') - args_to_be_kept2=np.array( list( set(args_early_ys2)-set(args_up2) ) ) + args_to_be_kept2=np.array(list( set(args_early_ys2)-set(args_up2) )) if len(args_to_be_kept2)>0: - y_type_2=np.array(y_type_2)[args_to_be_kept2] - x_starting=np.array(x_starting)[args_to_be_kept2] - x_ending=np.array(x_ending)[args_to_be_kept2] - y_diff_type_2=np.array(y_diff_type_2)[args_to_be_kept2] + y_type_2 = y_type_2[args_to_be_kept2] + x_starting = x_starting[args_to_be_kept2] + x_ending = x_ending[args_to_be_kept2] + y_diff_type_2 = y_diff_type_2[args_to_be_kept2] else: pass - #print('burdaydikh2') - - - elif len(y_diff_main_separator_up)==0: - nodes_in=[] + nodes_in = [] for ij in range(len(x_starting_up)): - nodes_in=nodes_in+list(np.array(range(x_starting_up[ij],x_ending_up[ij]))) - - #print(np.unique(nodes_in),'nodes_in2') + nodes_in = nodes_in + list(range(x_starting_up[ij], + x_ending_up[ij])) + nodes_in = np.unique(nodes_in) + #print(nodes_in,'nodes_in2') #print(np.array(range(len(peaks_neg_tot)-1)),'np.array(range(len(peaks_neg_tot)-1))') - - - if set(np.unique(nodes_in))==set(np.array(range(len(peaks_neg_tot)-1)) ): + if set(nodes_in)==set(range(len(peaks_neg_tot)-1)): pass - elif set(np.unique(nodes_in) )==set( np.array(range(1,len(peaks_neg_tot)-1)) ): + elif set(nodes_in)==set(range(1,len(peaks_neg_tot)-1)): pass else: #print('burdaydikh') #print(args_early_ys,'args_early_ys') #print(args_up,'args_up') - args_to_be_kept2=np.array( list( set(args_early_ys)-set(args_up) ) ) + args_to_be_kept2=np.array(list( set(args_early_ys) - set(args_up) )) #print(args_to_be_kept2,'args_to_be_kept2') - #print(len(y_type_2),len(x_starting),len(x_ending),len(y_diff_type_2)) - if len(args_to_be_kept2)>0: - y_type_2=np.array(y_type_2)[args_to_be_kept2] - x_starting=np.array(x_starting)[args_to_be_kept2] - x_ending=np.array(x_ending)[args_to_be_kept2] - y_diff_type_2=np.array(y_diff_type_2)[args_to_be_kept2] + y_type_2 = y_type_2[args_to_be_kept2] + x_starting = x_starting[args_to_be_kept2] + x_ending = x_ending[args_to_be_kept2] + y_diff_type_2 = y_diff_type_2[args_to_be_kept2] else: pass - #print('burdaydikh2') - - - - - - - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) - y_type_2=np.array(y_type_2) - y_diff_type_2_up=np.array(y_diff_type_2_up) #int(splitter_y_new[i]) - y_lines_by_order=[] x_start_by_order=[] x_end_by_order=[] - if (len(x_end_with_child_without_mother)==0 and reading_order_type==0) or reading_order_type==1: - - if reading_order_type==1: y_lines_by_order.append(int(splitter_y_new[i])) x_start_by_order.append(0) x_end_by_order.append(len(peaks_neg_tot)-2) else: #print(x_start_without_mother,x_end_without_mother,peaks_neg_tot,'dodo') - - columns_covered_by_mothers=[] - + columns_covered_by_mothers = [] for dj in range(len(x_start_without_mother)): - columns_covered_by_mothers=columns_covered_by_mothers+list(np.array(range(x_start_without_mother[dj],x_end_without_mother[dj])) ) - columns_covered_by_mothers=list(set(columns_covered_by_mothers)) - - all_columns=np.array(range(len(peaks_neg_tot)-1)) - - columns_not_covered=list( set(all_columns)-set(columns_covered_by_mothers) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - for lk in range(len(x_start_without_mother)): - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_start_without_mother[lk]) - x_ending.append(x_end_without_mother[lk]) - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) - - - + columns_covered_by_mothers = columns_covered_by_mothers + \ + list(range(x_start_without_mother[dj], + x_end_without_mother[dj])) + columns_covered_by_mothers = list(set(columns_covered_by_mothers)) - ind_args=np.array(range(len(y_type_2))) + all_columns=np.arange(len(peaks_neg_tot)-1) + columns_not_covered=list(set(all_columns) - set(columns_covered_by_mothers)) + y_type_2 = np.append(y_type_2, [int(splitter_y_new[i])] * (len(columns_not_covered) + len(x_start_without_mother))) + ##y_lines_by_order = np.append(y_lines_by_order, [int(splitter_y_new[i])] * len(columns_not_covered)) + ##x_start_by_order = np.append(x_start_by_order, [0] * len(columns_not_covered)) + x_starting = np.append(x_starting, columns_not_covered) + x_starting = np.append(x_starting, x_start_without_mother) + x_ending = np.append(x_ending, np.array(columns_not_covered) + 1) + x_ending = np.append(x_ending, x_end_without_mother) + + ind_args=np.arange(len(y_type_2)) #ind_args=np.array(ind_args) #print(ind_args,'ind_args') for column in range(len(peaks_neg_tot)-1): @@ -1920,159 +1885,115 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho y_lines_by_order.append(y_col_sort[ii]) x_start_by_order.append(x_start_column_sort[ii]) x_end_by_order.append(x_end_column_sort[ii]-1) - else: - #print(x_start_without_mother,x_end_without_mother,peaks_neg_tot,'dodo') - - columns_covered_by_mothers=[] - + columns_covered_by_mothers = [] for dj in range(len(x_start_without_mother)): - columns_covered_by_mothers=columns_covered_by_mothers+list(np.array(range(x_start_without_mother[dj],x_end_without_mother[dj])) ) - columns_covered_by_mothers=list(set(columns_covered_by_mothers)) + columns_covered_by_mothers = columns_covered_by_mothers + \ + list(range(x_start_without_mother[dj], + x_end_without_mother[dj])) + columns_covered_by_mothers = list(set(columns_covered_by_mothers)) - all_columns=np.array(range(len(peaks_neg_tot)-1)) - - columns_not_covered=list( set(all_columns)-set(columns_covered_by_mothers) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - for lk in range(len(x_start_without_mother)): - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_start_without_mother[lk]) - x_ending.append(x_end_without_mother[lk]) - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) - - columns_covered_by_with_child_no_mothers=[] + all_columns=np.arange(len(peaks_neg_tot)-1) + columns_not_covered=list(set(all_columns) - set(columns_covered_by_mothers)) + y_type_2 = np.append(y_type_2, [int(splitter_y_new[i])] * (len(columns_not_covered) + len(x_start_without_mother))) + ##y_lines_by_order = np.append(y_lines_by_order, [int(splitter_y_new[i])] * len(columns_not_covered)) + ##x_start_by_order = np.append(x_start_by_order, [0] * len(columns_not_covered)) + x_starting = np.append(x_starting, columns_not_covered) + x_starting = np.append(x_starting, x_start_without_mother) + x_ending = np.append(x_ending, np.array(columns_not_covered) + 1) + x_ending = np.append(x_ending, x_end_without_mother) + columns_covered_by_with_child_no_mothers = [] for dj in range(len(x_end_with_child_without_mother)): - columns_covered_by_with_child_no_mothers=columns_covered_by_with_child_no_mothers+list(np.array(range(x_start_with_child_without_mother[dj],x_end_with_child_without_mother[dj])) ) - columns_covered_by_with_child_no_mothers=list(set(columns_covered_by_with_child_no_mothers)) + columns_covered_by_with_child_no_mothers = columns_covered_by_with_child_no_mothers + \ + list(range(x_start_with_child_without_mother[dj], + x_end_with_child_without_mother[dj])) + columns_covered_by_with_child_no_mothers = list(set(columns_covered_by_with_child_no_mothers)) - all_columns=np.array(range(len(peaks_neg_tot)-1)) - - columns_not_covered_child_no_mother=list( set(all_columns)-set(columns_covered_by_with_child_no_mothers) ) + all_columns = np.arange(len(peaks_neg_tot)-1) + columns_not_covered_child_no_mother = list(set(all_columns) - set(columns_covered_by_with_child_no_mothers)) #indexes_to_be_spanned=[] - for i_s in range( len(x_end_with_child_without_mother) ): + for i_s in range(len(x_end_with_child_without_mother)): columns_not_covered_child_no_mother.append(x_start_with_child_without_mother[i_s]) - - - - columns_not_covered_child_no_mother=np.sort(columns_not_covered_child_no_mother) - - - - ind_args=np.array(range(len(y_type_2))) - - - + columns_not_covered_child_no_mother = np.sort(columns_not_covered_child_no_mother) + ind_args = np.arange(len(y_type_2)) + x_end_with_child_without_mother = np.array(x_end_with_child_without_mother) + x_start_with_child_without_mother = np.array(x_start_with_child_without_mother) for i_s_nc in columns_not_covered_child_no_mother: if i_s_nc in x_start_with_child_without_mother: - x_end_biggest_column=np.array(x_end_with_child_without_mother)[np.array(x_start_with_child_without_mother)==i_s_nc][0] - args_all_biggest_lines=ind_args[(x_starting==i_s_nc) & (x_ending==x_end_biggest_column)] - - args_all_biggest_lines=np.array(args_all_biggest_lines) - y_column_nc=y_type_2[args_all_biggest_lines] - x_start_column_nc=x_starting[args_all_biggest_lines] - x_end_column_nc=x_ending[args_all_biggest_lines] - - y_column_nc=np.sort(y_column_nc) - + x_end_biggest_column = x_end_with_child_without_mother[x_start_with_child_without_mother==i_s_nc][0] + args_all_biggest_lines = ind_args[(x_starting==i_s_nc) & + (x_ending==x_end_biggest_column)] + y_column_nc = y_type_2[args_all_biggest_lines] + x_start_column_nc = x_starting[args_all_biggest_lines] + x_end_column_nc = x_ending[args_all_biggest_lines] + y_column_nc = np.sort(y_column_nc) for i_c in range(len(y_column_nc)): if i_c==(len(y_column_nc)-1): - ind_all_lines_betweeen_nm_wc=ind_args[(y_type_2>y_column_nc[i_c]) & (y_type_2=i_s_nc) & (x_ending<=x_end_biggest_column)] + ind_all_lines_between_nm_wc=ind_args[(y_type_2>y_column_nc[i_c]) & + (y_type_2=i_s_nc) & + (x_ending<=x_end_biggest_column)] else: - ind_all_lines_betweeen_nm_wc=ind_args[(y_type_2>y_column_nc[i_c]) & (y_type_2=i_s_nc) & (x_ending<=x_end_biggest_column)] - - y_all_between_nm_wc=y_type_2[ind_all_lines_betweeen_nm_wc] - x_starting_all_between_nm_wc=x_starting[ind_all_lines_betweeen_nm_wc] - x_ending_all_between_nm_wc=x_ending[ind_all_lines_betweeen_nm_wc] - - x_diff_all_between_nm_wc=x_ending_all_between_nm_wc-x_starting_all_between_nm_wc - + ind_all_lines_between_nm_wc=ind_args[(y_type_2>y_column_nc[i_c]) & + (y_type_2=i_s_nc) & + (x_ending<=x_end_biggest_column)] + y_all_between_nm_wc = y_type_2[ind_all_lines_between_nm_wc] + x_starting_all_between_nm_wc = x_starting[ind_all_lines_between_nm_wc] + x_ending_all_between_nm_wc = x_ending[ind_all_lines_between_nm_wc] + x_diff_all_between_nm_wc = x_ending_all_between_nm_wc - x_starting_all_between_nm_wc if len(x_diff_all_between_nm_wc)>0: biggest=np.argmax(x_diff_all_between_nm_wc) - - columns_covered_by_mothers=[] - + columns_covered_by_mothers = [] for dj in range(len(x_starting_all_between_nm_wc)): - columns_covered_by_mothers=columns_covered_by_mothers+list(np.array(range(x_starting_all_between_nm_wc[dj],x_ending_all_between_nm_wc[dj])) ) - columns_covered_by_mothers=list(set(columns_covered_by_mothers)) + columns_covered_by_mothers = columns_covered_by_mothers + \ + list(range(x_starting_all_between_nm_wc[dj], + x_ending_all_between_nm_wc[dj])) + columns_covered_by_mothers = list(set(columns_covered_by_mothers)) - - all_columns=np.array(range(i_s_nc,x_end_biggest_column)) - - columns_not_covered=list( set(all_columns)-set(columns_covered_by_mothers) ) + all_columns=np.arange(i_s_nc, x_end_biggest_column) + columns_not_covered = list(set(all_columns) - set(columns_covered_by_mothers)) should_longest_line_be_extended=0 - if len(x_diff_all_between_nm_wc)>0 and set( list( np.array(range(x_starting_all_between_nm_wc[biggest],x_ending_all_between_nm_wc[biggest])) )+list(columns_not_covered) ) !=set(all_columns): + if (len(x_diff_all_between_nm_wc) > 0 and + set(list(range(x_starting_all_between_nm_wc[biggest], + x_ending_all_between_nm_wc[biggest])) + + list(columns_not_covered)) != set(all_columns)): should_longest_line_be_extended=1 - - index_lines_so_close_to_top_separator=np.array(range(len(y_all_between_nm_wc)))[(y_all_between_nm_wc>y_column_nc[i_c]) & (y_all_between_nm_wc<=(y_column_nc[i_c]+500))] - - - if len(index_lines_so_close_to_top_separator)>0: - indexes_remained_after_deleting_closed_lines= np.array( list ( set( list( np.array(range(len(y_all_between_nm_wc))) ) ) -set(list( index_lines_so_close_to_top_separator) ) ) ) - - if len(indexes_remained_after_deleting_closed_lines)>0: - y_all_between_nm_wc=y_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] - x_starting_all_between_nm_wc=x_starting_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] - x_ending_all_between_nm_wc=x_ending_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] - + index_lines_so_close_to_top_separator = \ + np.arange(len(y_all_between_nm_wc))[(y_all_between_nm_wc>y_column_nc[i_c]) & + (y_all_between_nm_wc<=(y_column_nc[i_c]+500))] + if len(index_lines_so_close_to_top_separator) > 0: + indexes_remained_after_deleting_closed_lines= \ + np.array(list(set(list(range(len(y_all_between_nm_wc)))) - + set(list(index_lines_so_close_to_top_separator)))) + if len(indexes_remained_after_deleting_closed_lines) > 0: + y_all_between_nm_wc = y_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] + x_starting_all_between_nm_wc = x_starting_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] + x_ending_all_between_nm_wc = x_ending_all_between_nm_wc[indexes_remained_after_deleting_closed_lines] - y_all_between_nm_wc=list(y_all_between_nm_wc) - x_starting_all_between_nm_wc=list(x_starting_all_between_nm_wc) - x_ending_all_between_nm_wc=list(x_ending_all_between_nm_wc) - - - y_all_between_nm_wc.append(y_column_nc[i_c] ) - x_starting_all_between_nm_wc.append(i_s_nc) - x_ending_all_between_nm_wc.append(x_end_biggest_column) - - + y_all_between_nm_wc = np.append(y_all_between_nm_wc, y_column_nc[i_c]) + x_starting_all_between_nm_wc = np.append(x_starting_all_between_nm_wc, i_s_nc) + x_ending_all_between_nm_wc = np.append(x_ending_all_between_nm_wc, x_end_biggest_column) - - y_all_between_nm_wc=list(y_all_between_nm_wc) - x_starting_all_between_nm_wc=list(x_starting_all_between_nm_wc) - x_ending_all_between_nm_wc=list(x_ending_all_between_nm_wc) - - if len(x_diff_all_between_nm_wc)>0: + if len(x_diff_all_between_nm_wc) > 0: try: - x_starting_all_between_nm_wc.append(x_starting_all_between_nm_wc[biggest]) - x_ending_all_between_nm_wc.append(x_ending_all_between_nm_wc[biggest]) - y_all_between_nm_wc.append(y_column_nc[i_c]) + y_all_between_nm_wc = np.append(y_all_between_nm_wc, y_column_nc[i_c]) + x_starting_all_between_nm_wc = np.append(x_starting_all_between_nm_wc, x_starting_all_between_nm_wc[biggest]) + x_ending_all_between_nm_wc = np.append(x_ending_all_between_nm_wc, x_ending_all_between_nm_wc[biggest]) except: pass - + y_all_between_nm_wc = np.append(y_all_between_nm_wc, [y_column_nc[i_c]] * len(columns_not_covered)) + x_starting_all_between_nm_wc = np.append(x_starting_all_between_nm_wc, columns_not_covered) + x_ending_all_between_nm_wc = np.append(x_ending_all_between_nm_wc, np.array(columns_not_covered) + 1) - for c_n_c in columns_not_covered: - y_all_between_nm_wc.append(y_column_nc[i_c]) - x_starting_all_between_nm_wc.append(c_n_c) - x_ending_all_between_nm_wc.append(c_n_c+1) - - y_all_between_nm_wc=np.array(y_all_between_nm_wc) - x_starting_all_between_nm_wc=np.array(x_starting_all_between_nm_wc) - x_ending_all_between_nm_wc=np.array(x_ending_all_between_nm_wc) - - ind_args_between=np.array(range(len(x_ending_all_between_nm_wc))) - - for column in range(i_s_nc,x_end_biggest_column): + ind_args_between=np.arange(len(x_ending_all_between_nm_wc)) + for column in range(i_s_nc, x_end_biggest_column): ind_args_in_col=ind_args_between[x_starting_all_between_nm_wc==column] #print('babali2') #print(ind_args_in_col,'ind_args_in_col') @@ -2092,14 +2013,7 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho y_lines_by_order.append(y_col_sort[ii]) x_start_by_order.append(x_start_column_sort[ii]) x_end_by_order.append(x_end_column_sort[ii]-1) - - - - - - else: - #print(column,'column') ind_args_in_col=ind_args[x_starting==i_s_nc] #print('babali2') @@ -2119,15 +2033,11 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho y_lines_by_order.append(y_col_sort[ii]) x_start_by_order.append(x_start_column_sort[ii]) x_end_by_order.append(x_end_column_sort[ii]-1) - - for il in range(len(y_lines_by_order)): - - - y_copy=list( np.copy(y_lines_by_order) ) - x_start_copy=list( np.copy(x_start_by_order) ) - x_end_copy=list ( np.copy(x_end_by_order) ) + y_copy = list(y_lines_by_order) + x_start_copy = list(x_start_by_order) + x_end_copy = list(x_end_by_order) #print(y_copy,'y_copy') y_itself=y_copy.pop(il) @@ -2135,13 +2045,14 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho x_end_itself=x_end_copy.pop(il) #print(y_copy,'y_copy2') - - for column in range(x_start_itself,x_end_itself+1): + for column in range(x_start_itself, x_end_itself+1): #print(column,'cols') y_in_cols=[] for yic in range(len(y_copy)): #print('burda') - if y_copy[yic]>y_itself and column>=x_start_copy[yic] and column<=x_end_copy[yic]: + if (y_copy[yic]>y_itself and + column>=x_start_copy[yic] and + column<=x_end_copy[yic]): y_in_cols.append(y_copy[yic]) #print('burda2') #print(y_in_cols,'y_in_cols') @@ -2150,81 +2061,48 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho else: y_down=[int(splitter_y_new[i+1])][0] #print(y_itself,'y_itself') - boxes.append([peaks_neg_tot[column],peaks_neg_tot[column+1],y_itself,y_down]) + boxes.append([peaks_neg_tot[column], + peaks_neg_tot[column+1], + y_itself, + y_down]) except: - boxes.append([0,peaks_neg_tot[len(peaks_neg_tot)-1],int(splitter_y_new[i]),int(splitter_y_new[i+1])]) - - - + boxes.append([0, peaks_neg_tot[len(peaks_neg_tot)-1], + int(splitter_y_new[i]), int(splitter_y_new[i+1])]) else: y_lines_by_order=[] x_start_by_order=[] x_end_by_order=[] if len(x_starting)>0: - all_columns = np.array(range(len(peaks_neg_tot)-1)) - columns_covered_by_lines_covered_more_than_2col=[] - + all_columns = np.arange(len(peaks_neg_tot)-1) + columns_covered_by_lines_covered_more_than_2col = [] for dj in range(len(x_starting)): - if set( list(np.array(range(x_starting[dj],x_ending[dj])) ) ) == set(all_columns): + if set(list(range(x_starting[dj],x_ending[dj]))) == set(all_columns): pass else: - columns_covered_by_lines_covered_more_than_2col=columns_covered_by_lines_covered_more_than_2col+list(np.array(range(x_starting[dj],x_ending[dj])) ) - columns_covered_by_lines_covered_more_than_2col=list(set(columns_covered_by_lines_covered_more_than_2col)) - - - - columns_not_covered=list( set(all_columns)-set(columns_covered_by_lines_covered_more_than_2col) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - - #y_type_2.append(int(splitter_y_new[i])) - #x_starting.append(x_starting[0]) - #x_ending.append(x_ending[0]) - - if len(new_main_sep_y)>0: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(0) - x_ending.append(len(peaks_neg_tot)-1) + columns_covered_by_lines_covered_more_than_2col = columns_covered_by_lines_covered_more_than_2col + \ + list(range(x_starting[dj],x_ending[dj])) + columns_covered_by_lines_covered_more_than_2col = list(set(columns_covered_by_lines_covered_more_than_2col)) + columns_not_covered = list(set(all_columns) - set(columns_covered_by_lines_covered_more_than_2col)) + + y_type_2 = np.append(y_type_2, [int(splitter_y_new[i])] * (len(columns_not_covered) + 1)) + ##y_lines_by_order = np.append(y_lines_by_order, [int(splitter_y_new[i])] * len(columns_not_covered)) + ##x_start_by_order = np.append(x_start_by_order, [0] * len(columns_not_covered)) + x_starting = np.append(x_starting, columns_not_covered) + x_ending = np.append(x_ending, np.array(columns_not_covered) + 1) + if len(new_main_sep_y) > 0: + x_starting = np.append(x_starting, 0) + x_ending = np.append(x_ending, len(peaks_neg_tot)-1) else: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(x_starting[0]) - x_ending.append(x_ending[0]) - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) + x_starting = np.append(x_starting, x_starting[0]) + x_ending = np.append(x_ending, x_ending[0]) else: - all_columns=np.array(range(len(peaks_neg_tot)-1)) - columns_not_covered=list( set(all_columns) ) - - - y_type_2=list(y_type_2) - x_starting=list(x_starting) - x_ending=list(x_ending) - - for lj in columns_not_covered: - y_type_2.append(int(splitter_y_new[i])) - x_starting.append(lj) - x_ending.append(lj+1) - ##y_lines_by_order.append(int(splitter_y_new[i])) - ##x_start_by_order.append(0) - - - - y_type_2=np.array(y_type_2) - x_starting=np.array(x_starting) - x_ending=np.array(x_ending) + all_columns = np.arange(len(peaks_neg_tot)-1) + columns_not_covered = list(set(all_columns)) + y_type_2 = np.append(y_type_2, [int(splitter_y_new[i])] * len(columns_not_covered)) + ##y_lines_by_order = np.append(y_lines_by_order, [int(splitter_y_new[i])] * len(columns_not_covered)) + ##x_start_by_order = np.append(x_start_by_order, [0] * len(columns_not_covered)) + x_starting = np.append(x_starting, columns_not_covered) + x_ending = np.append(x_ending, np.array(columns_not_covered) + 1) ind_args=np.array(range(len(y_type_2))) #ind_args=np.array(ind_args) @@ -2248,13 +2126,10 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho x_start_by_order.append(x_start_column_sort[ii]) x_end_by_order.append(x_end_column_sort[ii]-1) - for il in range(len(y_lines_by_order)): - - - y_copy=list( np.copy(y_lines_by_order) ) - x_start_copy=list( np.copy(x_start_by_order) ) - x_end_copy=list ( np.copy(x_end_by_order) ) + y_copy = list(y_lines_by_order) + x_start_copy = list(x_start_by_order) + x_end_copy = list(x_end_by_order) #print(y_copy,'y_copy') y_itself=y_copy.pop(il) @@ -2262,13 +2137,14 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho x_end_itself=x_end_copy.pop(il) #print(y_copy,'y_copy2') - - for column in range(x_start_itself,x_end_itself+1): + for column in range(x_start_itself, x_end_itself+1): #print(column,'cols') y_in_cols=[] for yic in range(len(y_copy)): #print('burda') - if y_copy[yic]>y_itself and column>=x_start_copy[yic] and column<=x_end_copy[yic]: + if (y_copy[yic]>y_itself and + column>=x_start_copy[yic] and + column<=x_end_copy[yic]): y_in_cols.append(y_copy[yic]) #print('burda2') #print(y_in_cols,'y_in_cols') @@ -2277,10 +2153,10 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho else: y_down=[int(splitter_y_new[i+1])][0] #print(y_itself,'y_itself') - boxes.append([peaks_neg_tot[column],peaks_neg_tot[column+1],y_itself,y_down]) - - - + boxes.append([peaks_neg_tot[column], + peaks_neg_tot[column+1], + y_itself, + y_down]) #else: #boxes.append([ 0, regions_without_separators[:,:].shape[1] ,splitter_y_new[i],splitter_y_new[i+1]]) @@ -2291,7 +2167,6 @@ def return_boxes_of_images_by_order_of_reading_new(splitter_y_new, regions_witho peaks_neg_tot_tables_ind = regions_without_separators.shape[1] - np.array(peaks_tab_ind) peaks_neg_tot_tables_ind = list(peaks_neg_tot_tables_ind[::-1]) peaks_neg_tot_tables_new.append(peaks_neg_tot_tables_ind) - for i in range(len(boxes)): x_start_new = regions_without_separators.shape[1] - boxes[i][1] diff --git a/src/eynollah/utils/contour.py b/src/eynollah/utils/contour.py index e47c5e7..be00db0 100644 --- a/src/eynollah/utils/contour.py +++ b/src/eynollah/utils/contour.py @@ -27,35 +27,33 @@ def find_contours_mean_y_diff(contours_main): cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] return np.mean(np.diff(np.sort(np.array(cy_main)))) - def get_text_region_boxes_by_given_contours(contours): - kernel = np.ones((5, 5), np.uint8) boxes = [] contours_new = [] for jj in range(len(contours)): - x, y, w, h = cv2.boundingRect(contours[jj]) - - boxes.append([x, y, w, h]) + box = cv2.boundingRect(contours[jj]) + boxes.append(box) contours_new.append(contours[jj]) return boxes, contours_new def filter_contours_area_of_image(image, contours, hierarchy, max_area, min_area): - found_polygons_early = list() - + found_polygons_early = [] for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue polygon = geometry.Polygon([point[0] for point in c]) area = polygon.area - if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]) and hierarchy[0][jv][3] == -1: # and hierarchy[0][jv][3]==-1 : - found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.uint)) + if (area >= min_area * np.prod(image.shape[:2]) and + area <= max_area * np.prod(image.shape[:2]) and + hierarchy[0][jv][3] == -1): + found_polygons_early.append(np.array([[point] + for point in polygon.exterior.coords], dtype=np.uint)) return found_polygons_early def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, min_area): - found_polygons_early = list() - + found_polygons_early = [] for jv,c in enumerate(contours): if len(c) < 3: # A polygon cannot have less than 3 points continue @@ -66,48 +64,59 @@ def filter_contours_area_of_image_tables(image, contours, hierarchy, max_area, m ##print(np.prod(thresh.shape[:2])) # Check that polygon has area greater than minimal area # print(hierarchy[0][jv][3],hierarchy ) - if area >= min_area * np.prod(image.shape[:2]) and area <= max_area * np.prod(image.shape[:2]): # and hierarchy[0][jv][3]==-1 : + if (area >= min_area * np.prod(image.shape[:2]) and + area <= max_area * np.prod(image.shape[:2]) and + # hierarchy[0][jv][3]==-1 + True): # print(c[0][0][1]) - found_polygons_early.append(np.array([[point] for point in polygon.exterior.coords], dtype=np.int32)) + found_polygons_early.append(np.array([[point] + for point in polygon.exterior.coords], dtype=np.int32)) return found_polygons_early def find_new_features_of_contours(contours_main): - - areas_main = np.array([cv2.contourArea(contours_main[j]) for j in range(len(contours_main))]) - M_main = [cv2.moments(contours_main[j]) for j in range(len(contours_main))] - cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] - cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) for j in range(len(M_main))] + areas_main = np.array([cv2.contourArea(contours_main[j]) + for j in range(len(contours_main))]) + M_main = [cv2.moments(contours_main[j]) + for j in range(len(contours_main))] + cx_main = [(M_main[j]["m10"] / (M_main[j]["m00"] + 1e-32)) + for j in range(len(M_main))] + cy_main = [(M_main[j]["m01"] / (M_main[j]["m00"] + 1e-32)) + for j in range(len(M_main))] try: - x_min_main = np.array([np.min(contours_main[j][:, 0, 0]) for j in range(len(contours_main))]) - - argmin_x_main = np.array([np.argmin(contours_main[j][:, 0, 0]) for j in range(len(contours_main))]) - - x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0, 0] for j in range(len(contours_main))]) - y_corr_x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0, 1] for j in range(len(contours_main))]) - - x_max_main = np.array([np.max(contours_main[j][:, 0, 0]) for j in range(len(contours_main))]) - - y_min_main = np.array([np.min(contours_main[j][:, 0, 1]) for j in range(len(contours_main))]) - y_max_main = np.array([np.max(contours_main[j][:, 0, 1]) for j in range(len(contours_main))]) + x_min_main = np.array([np.min(contours_main[j][:, 0, 0]) + for j in range(len(contours_main))]) + argmin_x_main = np.array([np.argmin(contours_main[j][:, 0, 0]) + for j in range(len(contours_main))]) + x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0, 0] + for j in range(len(contours_main))]) + y_corr_x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0, 1] + for j in range(len(contours_main))]) + x_max_main = np.array([np.max(contours_main[j][:, 0, 0]) + for j in range(len(contours_main))]) + y_min_main = np.array([np.min(contours_main[j][:, 0, 1]) + for j in range(len(contours_main))]) + y_max_main = np.array([np.max(contours_main[j][:, 0, 1]) + for j in range(len(contours_main))]) except: - x_min_main = np.array([np.min(contours_main[j][:, 0]) for j in range(len(contours_main))]) - - argmin_x_main = np.array([np.argmin(contours_main[j][:, 0]) for j in range(len(contours_main))]) - - x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0] for j in range(len(contours_main))]) - y_corr_x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 1] for j in range(len(contours_main))]) - - x_max_main = np.array([np.max(contours_main[j][:, 0]) for j in range(len(contours_main))]) - - y_min_main = np.array([np.min(contours_main[j][:, 1]) for j in range(len(contours_main))]) - y_max_main = np.array([np.max(contours_main[j][:, 1]) for j in range(len(contours_main))]) - + x_min_main = np.array([np.min(contours_main[j][:, 0]) + for j in range(len(contours_main))]) + argmin_x_main = np.array([np.argmin(contours_main[j][:, 0]) + for j in range(len(contours_main))]) + x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 0] + for j in range(len(contours_main))]) + y_corr_x_min_from_argmin = np.array([contours_main[j][argmin_x_main[j], 1] + for j in range(len(contours_main))]) + x_max_main = np.array([np.max(contours_main[j][:, 0]) + for j in range(len(contours_main))]) + y_min_main = np.array([np.min(contours_main[j][:, 1]) + for j in range(len(contours_main))]) + y_max_main = np.array([np.max(contours_main[j][:, 1]) + for j in range(len(contours_main))]) # dis_x=np.abs(x_max_main-x_min_main) return cx_main, cy_main, x_min_main, x_max_main, y_min_main, y_max_main, y_corr_x_min_from_argmin -def find_features_of_contours(contours_main): - +def find_features_of_contours(contours_main): areas_main=np.array([cv2.contourArea(contours_main[j]) for j in range(len(contours_main))]) M_main=[cv2.moments(contours_main[j]) for j in range(len(contours_main))] cx_main=[(M_main[j]['m10']/(M_main[j]['m00']+1e-32)) for j in range(len(M_main))] @@ -118,14 +127,15 @@ def find_features_of_contours(contours_main): y_min_main=np.array([np.min(contours_main[j][:,0,1]) for j in range(len(contours_main))]) y_max_main=np.array([np.max(contours_main[j][:,0,1]) for j in range(len(contours_main))]) - return y_min_main, y_max_main + def return_parent_contours(contours, hierarchy): - contours_parent = [contours[i] for i in range(len(contours)) if hierarchy[0][i][3] == -1] + contours_parent = [contours[i] + for i in range(len(contours)) + if hierarchy[0][i][3] == -1] return contours_parent def return_contours_of_interested_region(region_pre_p, pixel, min_area=0.0002): - # pixels of images are identified by 5 if len(region_pre_p.shape) == 3: cnts_images = (region_pre_p[:, :, 0] == pixel) * 1 @@ -137,10 +147,9 @@ def return_contours_of_interested_region(region_pre_p, pixel, min_area=0.0002): ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_imgs, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - contours_imgs = return_parent_contours(contours_imgs, hierarchy) - contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, max_area=1, min_area=min_area) - + contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, + max_area=1, min_area=min_area) return contours_imgs def do_work_of_contours_in_image(contour, index_r_con, img, slope_first): @@ -148,7 +157,6 @@ def do_work_of_contours_in_image(contour, index_r_con, img, slope_first): img_copy = cv2.fillPoly(img_copy, pts=[contour], color=(1, 1, 1)) img_copy = rotation_image_new(img_copy, -slope_first) - img_copy = img_copy.astype(np.uint8) imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) @@ -158,7 +166,6 @@ def do_work_of_contours_in_image(contour, index_r_con, img, slope_first): cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) - return cont_int[0], index_r_con def get_textregion_contours_in_org_image_multi(cnts, img, slope_first, map=map): @@ -172,7 +179,6 @@ def get_textregion_contours_in_org_image_multi(cnts, img, slope_first, map=map): return tuple(zip(*results)) def get_textregion_contours_in_org_image(cnts, img, slope_first): - cnts_org = [] # print(cnts,'cnts') for i in range(len(cnts)): @@ -193,7 +199,6 @@ def get_textregion_contours_in_org_image(cnts, img, slope_first): ret, thresh = cv2.threshold(imgray, 0, 255, 0) cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) # print(np.shape(cont_int[0])) @@ -202,32 +207,23 @@ def get_textregion_contours_in_org_image(cnts, img, slope_first): return cnts_org def get_textregion_contours_in_org_image_light_old(cnts, img, slope_first): - - h_o = img.shape[0] - w_o = img.shape[1] - - img = cv2.resize(img, (int(img.shape[1]/3.), int(img.shape[0]/3.)), interpolation=cv2.INTER_NEAREST) - ##cnts = list( (np.array(cnts)/2).astype(np.int16) ) - #cnts = cnts/2 - cnts = [(i/ 3).astype(np.int32) for i in cnts] + zoom = 3 + img = cv2.resize(img, (img.shape[1] // zoom, + img.shape[0] // zoom), + interpolation=cv2.INTER_NEAREST) cnts_org = [] - #print(cnts,'cnts') - for i in range(len(cnts)): + for cnt in cnts: img_copy = np.zeros(img.shape) - img_copy = cv2.fillPoly(img_copy, pts=[cnts[i]], color=(1, 1, 1)) + img_copy = cv2.fillPoly(img_copy, pts=[(cnt / zoom).astype(int)], color=(1, 1, 1)) - img_copy = rotation_image_new(img_copy, -slope_first) - - img_copy = img_copy.astype(np.uint8) + img_copy = rotation_image_new(img_copy, -slope_first).astype(np.uint8) imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) - # print(np.shape(cont_int[0])) - cnts_org.append(cont_int[0]*3) + cnts_org.append(cont_int[0] * zoom) return cnts_org @@ -235,14 +231,11 @@ def do_back_rotation_and_get_cnt_back(contour_par, index_r_con, img, slope_first img_copy = np.zeros(img.shape) img_copy = cv2.fillPoly(img_copy, pts=[contour_par], color=(1, 1, 1)) - img_copy = rotation_image_new(img_copy, -slope_first) - - img_copy = img_copy.astype(np.uint8) + img_copy = rotation_image_new(img_copy, -slope_first).astype(np.uint8) imgray = cv2.cvtColor(img_copy, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(imgray, 0, 255, 0) cont_int, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - cont_int[0][:, 0, 0] = cont_int[0][:, 0, 0] + np.abs(img_copy.shape[1] - img.shape[1]) cont_int[0][:, 0, 1] = cont_int[0][:, 0, 1] + np.abs(img_copy.shape[0] - img.shape[0]) # print(np.shape(cont_int[0])) @@ -264,7 +257,6 @@ def get_textregion_contours_in_org_image_light(cnts, img, slope_first, map=map): return [i*6 for i in contours] def return_contours_of_interested_textline(region_pre_p, pixel): - # pixels of images are identified by 5 if len(region_pre_p.shape) == 3: cnts_images = (region_pre_p[:, :, 0] == pixel) * 1 @@ -277,11 +269,11 @@ def return_contours_of_interested_textline(region_pre_p, pixel): contours_imgs, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours_imgs = return_parent_contours(contours_imgs, hierarchy) - contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, max_area=1, min_area=0.000000003) + contours_imgs = filter_contours_area_of_image_tables( + thresh, contours_imgs, hierarchy, max_area=1, min_area=0.000000003) return contours_imgs def return_contours_of_image(image): - if len(image.shape) == 2: image = np.repeat(image[:, :, np.newaxis], 3, axis=2) image = image.astype(np.uint8) @@ -293,7 +285,6 @@ def return_contours_of_image(image): return contours, hierarchy def return_contours_of_interested_region_by_min_size(region_pre_p, pixel, min_size=0.00003): - # pixels of images are identified by 5 if len(region_pre_p.shape) == 3: cnts_images = (region_pre_p[:, :, 0] == pixel) * 1 @@ -305,14 +296,13 @@ def return_contours_of_interested_region_by_min_size(region_pre_p, pixel, min_si ret, thresh = cv2.threshold(imgray, 0, 255, 0) contours_imgs, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) - contours_imgs = return_parent_contours(contours_imgs, hierarchy) - contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, max_area=1, min_area=min_size) + contours_imgs = filter_contours_area_of_image_tables( + thresh, contours_imgs, hierarchy, max_area=1, min_area=min_size) return contours_imgs def return_contours_of_interested_region_by_size(region_pre_p, pixel, min_area, max_area): - # pixels of images are identified by 5 if len(region_pre_p.shape) == 3: cnts_images = (region_pre_p[:, :, 0] == pixel) * 1 @@ -325,9 +315,11 @@ def return_contours_of_interested_region_by_size(region_pre_p, pixel, min_area, contours_imgs, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours_imgs = return_parent_contours(contours_imgs, hierarchy) - contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, max_area=max_area, min_area=min_area) + contours_imgs = filter_contours_area_of_image_tables( + thresh, contours_imgs, hierarchy, max_area=max_area, min_area=min_area) img_ret = np.zeros((region_pre_p.shape[0], region_pre_p.shape[1], 3)) img_ret = cv2.fillPoly(img_ret, pts=contours_imgs, color=(1, 1, 1)) + return img_ret[:, :, 0] diff --git a/src/eynollah/utils/separate_lines.py b/src/eynollah/utils/separate_lines.py index f037a9f..7e77afe 100644 --- a/src/eynollah/utils/separate_lines.py +++ b/src/eynollah/utils/separate_lines.py @@ -41,9 +41,7 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): y_max_cont = img_patch.shape[0] xv = np.linspace(x_min_cont, x_max_cont, 1000) - textline_patch_sum_along_width = img_patch.sum(axis=axis) - first_nonzero = 0 # (next((i for i, x in enumerate(mada_n) if x), None)) y = textline_patch_sum_along_width[:] # [first_nonzero:last_nonzero] @@ -52,11 +50,8 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): x = np.array(range(len(y))) peaks_real, _ = find_peaks(gaussian_filter1d(y, 3), height=0) - if 1 > 0: - try: - y_padded_smoothed_e = gaussian_filter1d(y_padded, 2) y_padded_up_to_down_e = -y_padded + np.max(y_padded) y_padded_up_to_down_padded_e = np.zeros(len(y_padded_up_to_down_e) + 40) @@ -67,7 +62,7 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): peaks_neg_e, _ = find_peaks(y_padded_up_to_down_padded_e, height=0) neg_peaks_max = np.max(y_padded_up_to_down_padded_e[peaks_neg_e]) - arg_neg_must_be_deleted = np.array(range(len(peaks_neg_e)))[y_padded_up_to_down_padded_e[peaks_neg_e] / float(neg_peaks_max) < 0.3] + arg_neg_must_be_deleted = np.arange(len(peaks_neg_e))[y_padded_up_to_down_padded_e[peaks_neg_e] / float(neg_peaks_max) < 0.3] diff_arg_neg_must_be_deleted = np.diff(arg_neg_must_be_deleted) arg_diff = np.array(range(len(diff_arg_neg_must_be_deleted))) @@ -78,12 +73,11 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): clusters_to_be_deleted = [] if len(arg_diff_cluster) > 0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0 : arg_diff_cluster[0] + 1]) for i in range(len(arg_diff_cluster) - 1): - clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : arg_diff_cluster[i + 1] + 1]) + clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : + arg_diff_cluster[i + 1] + 1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster) - 1] + 1 :]) - if len(clusters_to_be_deleted) > 0: peaks_new_extra = [] for m in range(len(clusters_to_be_deleted)): @@ -93,7 +87,6 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new = peaks_new[peaks_new != peaks_e[clusters_to_be_deleted[m][m1] - 1]] peaks_new = peaks_new[peaks_new != peaks_e[clusters_to_be_deleted[m][m1]]] - peaks_neg_new = peaks_neg_new[peaks_neg_new != peaks_neg_e[clusters_to_be_deleted[m][m1]]] peaks_new_tot = [] for i1 in peaks_new: @@ -106,9 +99,10 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): peaks_new_tot = peaks_e[:] textline_con, hierarchy = return_contours_of_image(img_patch) - textline_con_fil = filter_contours_area_of_image(img_patch, textline_con, hierarchy, max_area=1, min_area=0.0008) + textline_con_fil = filter_contours_area_of_image(img_patch, + textline_con, hierarchy, + max_area=1, min_area=0.0008) y_diff_mean = np.mean(np.diff(peaks_new_tot)) # self.find_contours_mean_y_diff(textline_con_fil) - sigma_gaus = int(y_diff_mean * (7.0 / 40.0)) # print(sigma_gaus,'sigma_gaus') except: @@ -126,10 +120,18 @@ def dedup_separate_lines(img_patch, contour_text_interest, thetha, axis): peaks, _ = find_peaks(y_padded_smoothed, height=0) peaks_neg, _ = find_peaks(y_padded_up_to_down_padded, height=0) - return x, y, x_d, y_d, xv, x_min_cont, y_min_cont, x_max_cont, y_max_cont, first_nonzero, y_padded_up_to_down_padded, y_padded_smoothed, peaks, peaks_neg, rotation_matrix + return (x, y, + x_d, y_d, + xv, + x_min_cont, y_min_cont, + x_max_cont, y_max_cont, + first_nonzero, + y_padded_up_to_down_padded, + y_padded_smoothed, + peaks, peaks_neg, + rotation_matrix) def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): - (h, w) = img_patch.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, -thetha, 1.0) @@ -151,9 +153,7 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): y_max_cont = img_patch.shape[0] xv = np.linspace(x_min_cont, x_max_cont, 1000) - textline_patch_sum_along_width = img_patch.sum(axis=1) - first_nonzero = 0 # (next((i for i, x in enumerate(mada_n) if x), None)) y = textline_patch_sum_along_width[:] # [first_nonzero:last_nonzero] @@ -162,11 +162,8 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): x = np.array(range(len(y))) peaks_real, _ = find_peaks(gaussian_filter1d(y, 3), height=0) - if 1>0: - try: - y_padded_smoothed_e= gaussian_filter1d(y_padded, 2) y_padded_up_to_down_e=-y_padded+np.max(y_padded) y_padded_up_to_down_padded_e=np.zeros(len(y_padded_up_to_down_e)+40) @@ -178,27 +175,22 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): peaks_neg_e, _ = find_peaks(y_padded_up_to_down_padded_e, height=0) neg_peaks_max=np.max(y_padded_up_to_down_padded_e[peaks_neg_e]) - arg_neg_must_be_deleted= np.array(range(len(peaks_neg_e)))[y_padded_up_to_down_padded_e[peaks_neg_e]/float(neg_peaks_max)<0.3 ] + arg_neg_must_be_deleted= np.arange(len(peaks_neg_e))[y_padded_up_to_down_padded_e[peaks_neg_e]/float(neg_peaks_max)<0.3] diff_arg_neg_must_be_deleted=np.diff(arg_neg_must_be_deleted) - - arg_diff=np.array(range(len(diff_arg_neg_must_be_deleted))) arg_diff_cluster=arg_diff[diff_arg_neg_must_be_deleted>1] - peaks_new=peaks_e[:] peaks_neg_new=peaks_neg_e[:] clusters_to_be_deleted=[] if len(arg_diff_cluster)>0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0:arg_diff_cluster[0]+1]) for i in range(len(arg_diff_cluster)-1): - clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i]+1:arg_diff_cluster[i+1]+1]) + clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i]+1: + arg_diff_cluster[i+1]+1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster)-1]+1:]) - - if len(clusters_to_be_deleted)>0: peaks_new_extra=[] for m in range(len(clusters_to_be_deleted)): @@ -208,7 +200,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new=peaks_new[peaks_new!=peaks_e[clusters_to_be_deleted[m][m1]-1]] peaks_new=peaks_new[peaks_new!=peaks_e[clusters_to_be_deleted[m][m1]]] - peaks_neg_new=peaks_neg_new[peaks_neg_new!=peaks_neg_e[clusters_to_be_deleted[m][m1]]] peaks_new_tot=[] for i1 in peaks_new: @@ -216,16 +207,14 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): for i1 in peaks_new_extra: peaks_new_tot.append(i1) peaks_new_tot=np.sort(peaks_new_tot) - - else: peaks_new_tot=peaks_e[:] - textline_con,hierarchy=return_contours_of_image(img_patch) - textline_con_fil=filter_contours_area_of_image(img_patch,textline_con,hierarchy,max_area=1,min_area=0.0008) + textline_con_fil=filter_contours_area_of_image(img_patch, + textline_con, hierarchy, + max_area=1, min_area=0.0008) y_diff_mean=np.mean(np.diff(peaks_new_tot))#self.find_contours_mean_y_diff(textline_con_fil) - sigma_gaus=int( y_diff_mean * (7./40.0) ) #print(sigma_gaus,'sigma_gaus') except: @@ -234,60 +223,41 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): sigma_gaus=3 #print(sigma_gaus,'sigma') - y_padded_smoothed= gaussian_filter1d(y_padded, sigma_gaus) y_padded_up_to_down=-y_padded+np.max(y_padded) y_padded_up_to_down_padded=np.zeros(len(y_padded_up_to_down)+40) y_padded_up_to_down_padded[20:len(y_padded_up_to_down)+20]=y_padded_up_to_down y_padded_up_to_down_padded= gaussian_filter1d(y_padded_up_to_down_padded, sigma_gaus) - peaks, _ = find_peaks(y_padded_smoothed, height=0) peaks_neg, _ = find_peaks(y_padded_up_to_down_padded, height=0) - - - - try: neg_peaks_max=np.max(y_padded_smoothed[peaks]) - - - arg_neg_must_be_deleted= np.array(range(len(peaks_neg)))[y_padded_up_to_down_padded[peaks_neg]/float(neg_peaks_max)<0.42 ] - - + arg_neg_must_be_deleted= np.arange(len(peaks_neg))[y_padded_up_to_down_padded[peaks_neg]/float(neg_peaks_max)<0.42] diff_arg_neg_must_be_deleted=np.diff(arg_neg_must_be_deleted) - - arg_diff=np.array(range(len(diff_arg_neg_must_be_deleted))) arg_diff_cluster=arg_diff[diff_arg_neg_must_be_deleted>1] except: arg_neg_must_be_deleted=[] arg_diff_cluster=[] - - try: peaks_new=peaks[:] peaks_neg_new=peaks_neg[:] clusters_to_be_deleted=[] - if len(arg_diff_cluster)>=2 and len(arg_diff_cluster)>0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0:arg_diff_cluster[0]+1]) for i in range(len(arg_diff_cluster)-1): - clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i]+1:arg_diff_cluster[i+1]+1]) + clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i]+1: + arg_diff_cluster[i+1]+1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster)-1]+1:]) elif len(arg_neg_must_be_deleted)>=2 and len(arg_diff_cluster)==0: clusters_to_be_deleted.append(arg_neg_must_be_deleted[:]) - - if len(arg_neg_must_be_deleted)==1: clusters_to_be_deleted.append(arg_neg_must_be_deleted) - - if len(clusters_to_be_deleted)>0: peaks_new_extra=[] for m in range(len(clusters_to_be_deleted)): @@ -297,7 +267,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new=peaks_new[peaks_new!=peaks[clusters_to_be_deleted[m][m1]-1]] peaks_new=peaks_new[peaks_new!=peaks[clusters_to_be_deleted[m][m1]]] - peaks_neg_new=peaks_neg_new[peaks_neg_new!=peaks_neg[clusters_to_be_deleted[m][m1]]] peaks_new_tot=[] for i1 in peaks_new: @@ -321,36 +290,27 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): ##plt.plot(y_padded_smoothed) ##plt.plot(peaks_new_tot,y_padded_smoothed[peaks_new_tot],'*') ##plt.show() - peaks=peaks_new_tot[:] peaks_neg=peaks_neg_new[:] - - else: peaks_new_tot=peaks[:] peaks=peaks_new_tot[:] peaks_neg=peaks_neg_new[:] except: pass - mean_value_of_peaks=np.mean(y_padded_smoothed[peaks]) std_value_of_peaks=np.std(y_padded_smoothed[peaks]) peaks_values=y_padded_smoothed[peaks] - peaks_neg = peaks_neg - 20 - 20 peaks = peaks - 20 - for jj in range(len(peaks_neg)): if peaks_neg[jj] > len(x) - 1: peaks_neg[jj] = len(x) - 1 - for jj in range(len(peaks)): if peaks[jj] > len(x) - 1: peaks[jj] = len(x) - 1 - - textline_boxes = [] textline_boxes_rot = [] @@ -386,7 +346,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): 1.1 * dis_to_next_down) ###-int(dis_to_next_down*1./2) - if point_down_narrow >= img_patch.shape[0]: point_down_narrow = img_patch.shape[0] - 2 @@ -423,8 +382,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): if point_up_rot2<0: point_up_rot2=0 - - x_min_rot1=x_min_rot1-x_help x_max_rot2=x_max_rot2-x_help x_max_rot3=x_max_rot3-x_help @@ -435,29 +392,24 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_down_rot3=point_down_rot3-y_help point_down_rot4=point_down_rot4-y_help - - - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) - elif len(peaks) < 1: pass elif len(peaks) == 1: - distances = [cv2.pointPolygonTest(contour_text_interest_copy, tuple(int(x) for x in np.array([xv[mj], peaks[0] + first_nonzero])), True) - for mj in range(len(xv))] + distances = [cv2.pointPolygonTest(contour_text_interest_copy, + tuple(int(x) for x in np.array([xv[mj], peaks[0] + first_nonzero])), True) + for mj in range(len(xv))] distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -480,7 +432,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): x_max_rot3, point_down_rot3 = p3[0] + x_d, p3[1] + y_d x_min_rot4, point_down_rot4 = p4[0] + x_d, p4[1] + y_d - if x_min_rot1<0: x_min_rot1=0 if x_min_rot4<0: @@ -489,7 +440,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_up_rot1=0 if point_up_rot2<0: point_up_rot2=0 - x_min_rot1=x_min_rot1-x_help x_max_rot2=x_max_rot2-x_help @@ -500,22 +450,15 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_up_rot2=point_up_rot2-y_help point_down_rot3=point_down_rot3-y_help point_down_rot4=point_down_rot4-y_help - - - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - textline_boxes.append(np.array([[int(x_min), int(y_min)], [int(x_max), int(y_min)], [int(x_max), int(y_max)], [int(x_min), int(y_max)]])) - - - elif len(peaks) == 2: dis_to_next = np.abs(peaks[1] - peaks[0]) for jj in range(len(peaks)): @@ -533,12 +476,12 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): except: point_up =peaks[jj] + first_nonzero - int(1. / 1.8 * dis_to_next) - distances = [cv2.pointPolygonTest(contour_text_interest_copy, tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) - for mj in range(len(xv))] + distances = [cv2.pointPolygonTest(contour_text_interest_copy, + tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) + for mj in range(len(xv))] distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -556,8 +499,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): x_max_rot3, point_down_rot3 = p3[0] + x_d, p3[1] + y_d x_min_rot4, point_down_rot4 = p4[0] + x_d, p4[1] + y_d - - if x_min_rot1<0: x_min_rot1=0 if x_min_rot4<0: @@ -577,21 +518,16 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_down_rot3=point_down_rot3-y_help point_down_rot4=point_down_rot4-y_help - - - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) else: for jj in range(len(peaks)): - if jj == 0: dis_to_next = peaks[jj + 1] - peaks[jj] # point_up=peaks[jj]+first_nonzero-int(1./3*dis_to_next) @@ -615,12 +551,12 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_up = peaks[jj] + first_nonzero - int(1. / 1.9 * dis_to_next_up) point_down = peaks[jj] + first_nonzero + int(1. / 1.9 * dis_to_next_down) - distances = [cv2.pointPolygonTest(contour_text_interest_copy, tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) - for mj in range(len(xv))] + distances = [cv2.pointPolygonTest(contour_text_interest_copy, + tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) + for mj in range(len(xv))] distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -646,7 +582,6 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_up_rot1=0 if point_up_rot2<0: point_up_rot2=0 - x_min_rot1=x_min_rot1-x_help x_max_rot2=x_max_rot2-x_help @@ -657,29 +592,24 @@ def separate_lines(img_patch, contour_text_interest, thetha, x_help, y_help): point_up_rot2=point_up_rot2-y_help point_down_rot3=point_down_rot3-y_help point_down_rot4=point_down_rot4-y_help - - - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) - return peaks, textline_boxes_rot def separate_lines_vertical(img_patch, contour_text_interest, thetha): - thetha = thetha + 90 contour_text_interest_copy = contour_text_interest.copy() - x, y, x_d, y_d, xv, x_min_cont, y_min_cont, x_max_cont, y_max_cont, first_nonzero, y_padded_up_to_down_padded, y_padded_smoothed, peaks, peaks_neg, rotation_matrix = dedup_separate_lines(img_patch, contour_text_interest, thetha, 0) - + x, y, x_d, y_d, xv, x_min_cont, y_min_cont, x_max_cont, y_max_cont, \ + first_nonzero, y_padded_up_to_down_padded, y_padded_smoothed, \ + peaks, peaks_neg, rotation_matrix = dedup_separate_lines(img_patch, contour_text_interest, thetha, 0) # plt.plot(y_padded_up_to_down_padded) # plt.plot(peaks_neg,y_padded_up_to_down_padded[peaks_neg],'*') @@ -693,8 +623,7 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): neg_peaks_max = np.max(y_padded_up_to_down_padded[peaks_neg]) - arg_neg_must_be_deleted = np.array(range(len(peaks_neg)))[y_padded_up_to_down_padded[peaks_neg] / float(neg_peaks_max) < 0.42] - + arg_neg_must_be_deleted = np.arange(len(peaks_neg))[y_padded_up_to_down_padded[peaks_neg] / float(neg_peaks_max) < 0.42] diff_arg_neg_must_be_deleted = np.diff(arg_neg_must_be_deleted) arg_diff = np.array(range(len(diff_arg_neg_must_be_deleted))) @@ -705,17 +634,15 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): clusters_to_be_deleted = [] if len(arg_diff_cluster) >= 2 and len(arg_diff_cluster) > 0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0 : arg_diff_cluster[0] + 1]) for i in range(len(arg_diff_cluster) - 1): - clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : arg_diff_cluster[i + 1] + 1]) + clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : + arg_diff_cluster[i + 1] + 1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster) - 1] + 1 :]) elif len(arg_neg_must_be_deleted) >= 2 and len(arg_diff_cluster) == 0: clusters_to_be_deleted.append(arg_neg_must_be_deleted[:]) - if len(arg_neg_must_be_deleted) == 1: clusters_to_be_deleted.append(arg_neg_must_be_deleted) - if len(clusters_to_be_deleted) > 0: peaks_new_extra = [] for m in range(len(clusters_to_be_deleted)): @@ -725,7 +652,6 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new = peaks_new[peaks_new != peaks[clusters_to_be_deleted[m][m1] - 1]] peaks_new = peaks_new[peaks_new != peaks[clusters_to_be_deleted[m][m1]]] - peaks_neg_new = peaks_neg_new[peaks_neg_new != peaks_neg[clusters_to_be_deleted[m][m1]]] peaks_new_tot = [] for i1 in peaks_new: @@ -796,7 +722,6 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -823,13 +748,16 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): if point_up_rot2 < 0: point_up_rot2 = 0 - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) - + textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], + [int(x_max_rot2), int(point_up_rot2)], + [int(x_max_rot3), int(point_down_rot3)], + [int(x_min_rot4), int(point_down_rot4)]])) + textline_boxes.append(np.array([[int(x_min), int(point_up)], + [int(x_max), int(point_up)], + [int(x_max), int(point_down)], + [int(x_min), int(point_down)]])) elif len(peaks) < 1: pass - elif len(peaks) == 1: x_min = x_min_cont x_max = x_max_cont @@ -856,10 +784,14 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): if point_up_rot2 < 0: point_up_rot2 = 0 - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - - textline_boxes.append(np.array([[int(x_min), int(y_min)], [int(x_max), int(y_min)], [int(x_max), int(y_max)], [int(x_min), int(y_max)]])) - + textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], + [int(x_max_rot2), int(point_up_rot2)], + [int(x_max_rot3), int(point_down_rot3)], + [int(x_min_rot4), int(point_down_rot4)]])) + textline_boxes.append(np.array([[int(x_min), int(y_min)], + [int(x_max), int(y_min)], + [int(x_max), int(y_max)], + [int(x_min), int(y_max)]])) elif len(peaks) == 2: dis_to_next = np.abs(peaks[1] - peaks[0]) for jj in range(len(peaks)): @@ -874,11 +806,12 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): point_down = img_patch.shape[0] - 2 point_up = peaks[jj] + first_nonzero - int(1.0 / 1.8 * dis_to_next) - distances = [cv2.pointPolygonTest(contour_text_interest_copy, tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) for mj in range(len(xv))] + distances = [cv2.pointPolygonTest(contour_text_interest_copy, + tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) + for mj in range(len(xv))] distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -905,12 +838,16 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): if point_up_rot2 < 0: point_up_rot2 = 0 - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) + textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], + [int(x_max_rot2), int(point_up_rot2)], + [int(x_max_rot3), int(point_down_rot3)], + [int(x_min_rot4), int(point_down_rot4)]])) + textline_boxes.append(np.array([[int(x_min), int(point_up)], + [int(x_max), int(point_up)], + [int(x_max), int(point_down)], + [int(x_min), int(point_down)]])) else: for jj in range(len(peaks)): - if jj == 0: dis_to_next = peaks[jj + 1] - peaks[jj] # point_up=peaks[jj]+first_nonzero-int(1./3*dis_to_next) @@ -934,11 +871,12 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): point_up = peaks[jj] + first_nonzero - int(1.0 / 1.9 * dis_to_next_up) point_down = peaks[jj] + first_nonzero + int(1.0 / 1.9 * dis_to_next_down) - distances = [cv2.pointPolygonTest(contour_text_interest_copy, tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) for mj in range(len(xv))] + distances = [cv2.pointPolygonTest(contour_text_interest_copy, + tuple(int(x) for x in np.array([xv[mj], peaks[jj] + first_nonzero])), True) + for mj in range(len(xv))] distances = np.array(distances) xvinside = xv[distances >= 0] - if len(xvinside) == 0: x_min = x_min_cont x_max = x_max_cont @@ -965,14 +903,17 @@ def separate_lines_vertical(img_patch, contour_text_interest, thetha): if point_up_rot2 < 0: point_up_rot2 = 0 - textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], [int(x_max_rot2), int(point_up_rot2)], [int(x_max_rot3), int(point_down_rot3)], [int(x_min_rot4), int(point_down_rot4)]])) - - textline_boxes.append(np.array([[int(x_min), int(point_up)], [int(x_max), int(point_up)], [int(x_max), int(point_down)], [int(x_min), int(point_down)]])) - + textline_boxes_rot.append(np.array([[int(x_min_rot1), int(point_up_rot1)], + [int(x_max_rot2), int(point_up_rot2)], + [int(x_max_rot3), int(point_down_rot3)], + [int(x_min_rot4), int(point_down_rot4)]])) + textline_boxes.append(np.array([[int(x_min), int(point_up)], + [int(x_max), int(point_up)], + [int(x_max), int(point_down)], + [int(x_min), int(point_down)]])) return peaks, textline_boxes_rot def separate_lines_new_inside_tiles2(img_patch, thetha): - (h, w) = img_patch.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, -thetha, 1.0) @@ -994,9 +935,7 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): y_max_cont = img_patch.shape[0] xv = np.linspace(x_min_cont, x_max_cont, 1000) - textline_patch_sum_along_width = img_patch.sum(axis=1) - first_nonzero = 0 # (next((i for i, x in enumerate(mada_n) if x), None)) y = textline_patch_sum_along_width[:] # [first_nonzero:last_nonzero] @@ -1006,9 +945,7 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): peaks_real, _ = find_peaks(gaussian_filter1d(y, 3), height=0) if 1 > 0: - try: - y_padded_smoothed_e = gaussian_filter1d(y_padded, 2) y_padded_up_to_down_e = -y_padded + np.max(y_padded) y_padded_up_to_down_padded_e = np.zeros(len(y_padded_up_to_down_e) + 40) @@ -1019,7 +956,7 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): peaks_neg_e, _ = find_peaks(y_padded_up_to_down_padded_e, height=0) neg_peaks_max = np.max(y_padded_up_to_down_padded_e[peaks_neg_e]) - arg_neg_must_be_deleted = np.array(range(len(peaks_neg_e)))[y_padded_up_to_down_padded_e[peaks_neg_e] / float(neg_peaks_max) < 0.3] + arg_neg_must_be_deleted = np.arange(len(peaks_neg_e))[y_padded_up_to_down_padded_e[peaks_neg_e] / float(neg_peaks_max) < 0.3] diff_arg_neg_must_be_deleted = np.diff(arg_neg_must_be_deleted) arg_diff = np.array(range(len(diff_arg_neg_must_be_deleted))) @@ -1030,12 +967,10 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): clusters_to_be_deleted = [] if len(arg_diff_cluster) > 0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0 : arg_diff_cluster[0] + 1]) for i in range(len(arg_diff_cluster) - 1): clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : arg_diff_cluster[i + 1] + 1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster) - 1] + 1 :]) - if len(clusters_to_be_deleted) > 0: peaks_new_extra = [] for m in range(len(clusters_to_be_deleted)): @@ -1045,7 +980,6 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new = peaks_new[peaks_new != peaks_e[clusters_to_be_deleted[m][m1] - 1]] peaks_new = peaks_new[peaks_new != peaks_e[clusters_to_be_deleted[m][m1]]] - peaks_neg_new = peaks_neg_new[peaks_neg_new != peaks_neg_e[clusters_to_be_deleted[m][m1]]] peaks_new_tot = [] for i1 in peaks_new: @@ -1053,12 +987,13 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): for i1 in peaks_new_extra: peaks_new_tot.append(i1) peaks_new_tot = np.sort(peaks_new_tot) - else: peaks_new_tot = peaks_e[:] textline_con, hierarchy = return_contours_of_image(img_patch) - textline_con_fil = filter_contours_area_of_image(img_patch, textline_con, hierarchy, max_area=1, min_area=0.0008) + textline_con_fil = filter_contours_area_of_image(img_patch, + textline_con, hierarchy, + max_area=1, min_area=0.0008) y_diff_mean = np.mean(np.diff(peaks_new_tot)) # self.find_contours_mean_y_diff(textline_con_fil) sigma_gaus = int(y_diff_mean * (7.0 / 40.0)) @@ -1084,27 +1019,23 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): try: neg_peaks_max = np.max(y_padded_smoothed[peaks]) - arg_neg_must_be_deleted = np.array(range(len(peaks_neg)))[y_padded_up_to_down_padded[peaks_neg] / float(neg_peaks_max) < 0.24] - + arg_neg_must_be_deleted = np.arange(len(peaks_neg))[y_padded_up_to_down_padded[peaks_neg] / float(neg_peaks_max) < 0.24] diff_arg_neg_must_be_deleted = np.diff(arg_neg_must_be_deleted) arg_diff = np.array(range(len(diff_arg_neg_must_be_deleted))) arg_diff_cluster = arg_diff[diff_arg_neg_must_be_deleted > 1] clusters_to_be_deleted = [] - if len(arg_diff_cluster) >= 2 and len(arg_diff_cluster) > 0: - clusters_to_be_deleted.append(arg_neg_must_be_deleted[0 : arg_diff_cluster[0] + 1]) for i in range(len(arg_diff_cluster) - 1): - clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : arg_diff_cluster[i + 1] + 1]) + clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[i] + 1 : + arg_diff_cluster[i + 1] + 1]) clusters_to_be_deleted.append(arg_neg_must_be_deleted[arg_diff_cluster[len(arg_diff_cluster) - 1] + 1 :]) elif len(arg_neg_must_be_deleted) >= 2 and len(arg_diff_cluster) == 0: clusters_to_be_deleted.append(arg_neg_must_be_deleted[:]) - if len(arg_neg_must_be_deleted) == 1: clusters_to_be_deleted.append(arg_neg_must_be_deleted) - if len(clusters_to_be_deleted) > 0: peaks_new_extra = [] for m in range(len(clusters_to_be_deleted)): @@ -1114,7 +1045,6 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): for m1 in range(len(clusters_to_be_deleted[m])): peaks_new = peaks_new[peaks_new != peaks[clusters_to_be_deleted[m][m1] - 1]] peaks_new = peaks_new[peaks_new != peaks[clusters_to_be_deleted[m][m1]]] - peaks_neg_new = peaks_neg_new[peaks_neg_new != peaks_neg[clusters_to_be_deleted[m][m1]]] peaks_new_tot = [] for i1 in peaks_new: @@ -1138,7 +1068,6 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): # plt.plot(y_padded_smoothed) # plt.plot(peaks_new_tot,y_padded_smoothed[peaks_new_tot],'*') # plt.show() - peaks = peaks_new_tot[:] peaks_neg = peaks_neg_new[:] except: @@ -1166,7 +1095,6 @@ def separate_lines_new_inside_tiles2(img_patch, thetha): # print(peaks_neg_true) for i in range(len(peaks_neg_true)): img_patch[peaks_neg_true[i] - 6 : peaks_neg_true[i] + 6, :] = 0 - else: pass @@ -1346,14 +1274,14 @@ def separate_lines_vertical_cont(img_patch, contour_text_interest, thetha, box_i contours_imgs, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours_imgs = return_parent_contours(contours_imgs, hierarchy) - contours_imgs = filter_contours_area_of_image_tables(thresh, contours_imgs, hierarchy, max_area=max_area, min_area=min_area) - + contours_imgs = filter_contours_area_of_image_tables(thresh, + contours_imgs, hierarchy, + max_area=max_area, min_area=min_area) cont_final = [] ###print(add_boxes_coor_into_textlines,'ikki') for i in range(len(contours_imgs)): img_contour = np.zeros((cnts_images.shape[0], cnts_images.shape[1], 3)) img_contour = cv2.fillPoly(img_contour, pts=[contours_imgs[i]], color=(255, 255, 255)) - img_contour = img_contour.astype(np.uint8) img_contour = cv2.dilate(img_contour, kernel, iterations=4) @@ -1373,9 +1301,7 @@ def separate_lines_vertical_cont(img_patch, contour_text_interest, thetha, box_i ##print(cont_final,'nadizzzz') return None, cont_final - def textline_contours_postprocessing(textline_mask, slope, contour_text_interest, box_ind, add_boxes_coor_into_textlines=False): - textline_mask = np.repeat(textline_mask[:, :, np.newaxis], 3, axis=2) * 255 textline_mask = textline_mask.astype(np.uint8) kernel = np.ones((5, 5), np.uint8) @@ -1400,8 +1326,10 @@ def textline_contours_postprocessing(textline_mask, slope, contour_text_interest x_help = 30 y_help = 2 - textline_mask_help = np.zeros((textline_mask.shape[0] + int(2 * y_help), textline_mask.shape[1] + int(2 * x_help), 3)) - textline_mask_help[y_help : y_help + textline_mask.shape[0], x_help : x_help + textline_mask.shape[1], :] = np.copy(textline_mask[:, :, :]) + textline_mask_help = np.zeros((textline_mask.shape[0] + int(2 * y_help), + textline_mask.shape[1] + int(2 * x_help), 3)) + textline_mask_help[y_help : y_help + textline_mask.shape[0], + x_help : x_help + textline_mask.shape[1], :] = np.copy(textline_mask[:, :, :]) dst = rotate_image(textline_mask_help, slope) dst = dst[:, :, 0] @@ -1412,7 +1340,6 @@ def textline_contours_postprocessing(textline_mask, slope, contour_text_interest # plt.show() contour_text_copy = contour_text_interest.copy() - contour_text_copy[:, 0, 0] = contour_text_copy[:, 0, 0] - box_ind[0] contour_text_copy[:, 0, 1] = contour_text_copy[:, 0, 1] - box_ind[1] @@ -1423,12 +1350,12 @@ def textline_contours_postprocessing(textline_mask, slope, contour_text_interest # plt.imshow(img_contour) # plt.show() - img_contour_help = np.zeros((img_contour.shape[0] + int(2 * y_help), img_contour.shape[1] + int(2 * x_help), 3)) - - img_contour_help[y_help : y_help + img_contour.shape[0], x_help : x_help + img_contour.shape[1], :] = np.copy(img_contour[:, :, :]) + img_contour_help = np.zeros((img_contour.shape[0] + int(2 * y_help), + img_contour.shape[1] + int(2 * x_help), 3)) + img_contour_help[y_help : y_help + img_contour.shape[0], + x_help : x_help + img_contour.shape[1], :] = np.copy(img_contour[:, :, :]) img_contour_rot = rotate_image(img_contour_help, slope) - # plt.imshow(img_contour_rot_help) # plt.show() @@ -1454,12 +1381,13 @@ def textline_contours_postprocessing(textline_mask, slope, contour_text_interest # print('juzaa') if abs(slope) > 45: # print(add_boxes_coor_into_textlines,'avval') - _, contours_rotated_clean = separate_lines_vertical_cont(textline_mask, contours_text_rot[ind_big_con], box_ind, slope, add_boxes_coor_into_textlines=add_boxes_coor_into_textlines) + _, contours_rotated_clean = separate_lines_vertical_cont( + textline_mask, contours_text_rot[ind_big_con], box_ind, slope, + add_boxes_coor_into_textlines=add_boxes_coor_into_textlines) else: - _, contours_rotated_clean = separate_lines(dst, contours_text_rot[ind_big_con], slope, x_help, y_help) - + _, contours_rotated_clean = separate_lines( + dst, contours_text_rot[ind_big_con], slope, x_help, y_help) except: - contours_rotated_clean = [] return contours_rotated_clean @@ -1487,11 +1415,9 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, logger=None, pl # print(margin,'margin') # if margin<=4: # margin = int(0.08 * length_x) - # margin=0 width_mid = length_x - 2 * margin - nxf = img_path.shape[1] / float(width_mid) if nxf > int(nxf): @@ -1553,8 +1479,8 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, logger=None, pl img_int[:, :] = img_xline[:, :] # img_patch_org[:,:,0] img_resized = np.zeros((int(img_int.shape[0] * (1.2)), int(img_int.shape[1] * (3)))) - - img_resized[int(img_int.shape[0] * (0.1)) : int(img_int.shape[0] * (0.1)) + img_int.shape[0], int(img_int.shape[1] * (1)) : int(img_int.shape[1] * (1)) + img_int.shape[1]] = img_int[:, :] + img_resized[int(img_int.shape[0] * (0.1)) : int(img_int.shape[0] * (0.1)) + img_int.shape[0], + int(img_int.shape[1] * (1.0)) : int(img_int.shape[1] * (1.0)) + img_int.shape[1]] = img_int[:, :] # plt.imshow(img_xline) # plt.show() img_line_rotated = rotate_image(img_resized, slopes_tile_wise[i]) @@ -1565,7 +1491,9 @@ def separate_lines_new2(img_path, thetha, num_col, slope_region, logger=None, pl img_patch_separated_returned = rotate_image(img_patch_separated, -slopes_tile_wise[i]) img_patch_separated_returned[:, :][img_patch_separated_returned[:, :] != 0] = 1 - img_patch_separated_returned_true_size = img_patch_separated_returned[int(img_int.shape[0] * (0.1)) : int(img_int.shape[0] * (0.1)) + img_int.shape[0], int(img_int.shape[1] * (1)) : int(img_int.shape[1] * (1)) + img_int.shape[1]] + img_patch_separated_returned_true_size = img_patch_separated_returned[ + int(img_int.shape[0] * (0.1)) : int(img_int.shape[0] * (0.1)) + img_int.shape[0], + int(img_int.shape[1] * (1.0)) : int(img_int.shape[1] * (1.0)) + img_int.shape[1]] img_patch_separated_returned_true_size = img_patch_separated_returned_true_size[:, margin : length_x - margin] img_patch_ineterst_revised[:, index_x_d + margin : index_x_u - margin] = img_patch_separated_returned_true_size @@ -1594,27 +1522,19 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, img_int=np.zeros((img_patch_org.shape[0],img_patch_org.shape[1])) img_int[:,:]=img_patch_org[:,:]#img_patch_org[:,:,0] - - max_shape=np.max(img_int.shape) img_resized=np.zeros((int( max_shape*(1.1) ) , int( max_shape*(1.1) ) )) - onset_x=int((img_resized.shape[1]-img_int.shape[1])/2.) onset_y=int((img_resized.shape[0]-img_int.shape[0])/2.) - #img_resized=np.zeros((int( img_int.shape[0]*(1.8) ) , int( img_int.shape[1]*(2.6) ) )) - - - #img_resized[ int( img_int.shape[0]*(.4)):int( img_int.shape[0]*(.4))+img_int.shape[0] , int( img_int.shape[1]*(.8)):int( img_int.shape[1]*(.8))+img_int.shape[1] ]=img_int[:,:] img_resized[ onset_y:onset_y+img_int.shape[0] , onset_x:onset_x+img_int.shape[1] ]=img_int[:,:] #print(img_resized.shape,'img_resizedshape') #plt.imshow(img_resized) #plt.show() - if main_page and img_patch_org.shape[1] > img_patch_org.shape[0]: #plt.imshow(img_resized) #plt.show() @@ -1623,7 +1543,6 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, angles = np.linspace(angle - 22.5, angle + 22.5, n_tot_angles) angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) - elif main_page: #plt.imshow(img_resized) #plt.show() @@ -1637,7 +1556,6 @@ def return_deskew_slop(img_patch_org, sigma_des,n_tot_angles=100, else: angles = np.linspace(90, 12, n_tot_angles) angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) - else: angles = np.linspace(-25, 25, int(0.5 * n_tot_angles) + 10) angle = get_smallest_skew(img_resized, sigma_des, angles, map=map, logger=logger, plotter=plotter) @@ -1695,7 +1613,9 @@ def do_work_of_slopes_new( else: try: textline_con, hierarchy = return_contours_of_image(img_int_p) - textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.00008) + textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, + hierarchy, + max_area=1, min_area=0.00008) y_diff_mean = find_contours_mean_y_diff(textline_con_fil) if np.isnan(y_diff_mean): slope_for_all = MAX_SLOPE @@ -1733,7 +1653,6 @@ def do_work_of_slopes_new( return cnt_clean_rot, box_text, contour, contour_par, crop_coor, index_r_con, slope - def do_work_of_slopes_new_curved( box_text, contour, contour_par, index_r_con, textline_mask_tot_ea, image_page_rotated, mask_texts_only, num_col, scale_par, slope_deskew, @@ -1759,7 +1678,9 @@ def do_work_of_slopes_new_curved( else: try: textline_con, hierarchy = return_contours_of_image(img_int_p) - textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, hierarchy, max_area=1, min_area=0.0008) + textline_con_fil = filter_contours_area_of_image(img_int_p, textline_con, + hierarchy, + max_area=1, min_area=0.0008) y_diff_mean = find_contours_mean_y_diff(textline_con_fil) if np.isnan(y_diff_mean): slope_for_all = MAX_SLOPE @@ -1788,7 +1709,8 @@ def do_work_of_slopes_new_curved( textline_biggest_region = mask_biggest * textline_mask_tot_ea # print(slope_for_all,'slope_for_all') - textline_rotated_separated = separate_lines_new2(textline_biggest_region[y: y+h, x: x+w], 0, num_col, slope_for_all, + textline_rotated_separated = separate_lines_new2(textline_biggest_region[y: y+h, x: x+w], 0, + num_col, slope_for_all, logger=logger, plotter=plotter) # new line added From 33fda2f8be1378181bd51818f45149e62740ef36 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 26 Dec 2024 22:45:40 +0100 Subject: [PATCH 318/412] changing cnn ocr model name --- src/eynollah/eynollah.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 95033e9..1bc837b 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -2191,18 +2191,18 @@ class Eynollah: img = resize_image(img_org, int(img_org.shape[0] * scaler_h), int(img_org.shape[1] * scaler_w)) if not self.dir_in: - ###prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + prediction_textline = self.do_prediction(patches, img, model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3, thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, model_textline, n_batch_inference=3) + ###prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, model_textline, n_batch_inference=3) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) #prediction_textline[:,:][prediction_textline_nopatch[:,:]==0] = 0 else: - ##prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) + prediction_textline = self.do_prediction(patches, img, self.model_textline, marginal_of_patch_percent=0.15, n_batch_inference=3,thresholding_for_artificial_class_in_light_version=thresholding_for_artificial_class_in_light_version) - prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, self.model_textline, n_batch_inference=3) + ##prediction_textline = self.do_prediction_new_concept_scatter_nd(patches, img, self.model_textline, n_batch_inference=3) #if not thresholding_for_artificial_class_in_light_version: #if num_col_classifier==1: #prediction_textline_nopatch = self.do_prediction(False, img, model_textline) @@ -2482,17 +2482,17 @@ class Eynollah: if num_col_classifier == 1 or num_col_classifier == 2: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: - prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) - ###prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) + ##prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) + prediction_regions_org = self.do_prediction_new_concept(True, img_resized, model_region, n_batch_inference=1, thresholding_for_some_classes_in_light_version = True) else: prediction_regions_org = np.zeros((self.image_org.shape[0], self.image_org.shape[1], 3)) - prediction_regions_page = self.do_prediction_new_concept_scatter_nd(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) - ##prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + ###prediction_regions_page = self.do_prediction_new_concept_scatter_nd(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) + prediction_regions_page = self.do_prediction_new_concept(False, self.image_page_org_size, model_region, n_batch_inference=1, thresholding_for_artificial_class_in_light_version = True) prediction_regions_org[self.page_coord[0] : self.page_coord[1], self.page_coord[2] : self.page_coord[3],:] = prediction_regions_page else: model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - ###prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) - prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) + prediction_regions_org = self.do_prediction_new_concept(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) + ###prediction_regions_org = self.do_prediction_new_concept_scatter_nd(True, resize_image(img_bin, int( (900+ (num_col_classifier-3)*100) *(img_bin.shape[0]/img_bin.shape[1]) ), 900+ (num_col_classifier-3)*100), model_region, n_batch_inference=2, thresholding_for_some_classes_in_light_version=True) ##model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_ens_light) ##prediction_regions_org = self.do_prediction(True, img_bin, model_region, n_batch_inference=3, thresholding_for_some_classes_in_light_version=True) else: @@ -5638,7 +5638,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_1_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( From 25116a2c79440ea16d8a5e28c6f1b7f08da5c6b6 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 19 Feb 2025 00:35:48 +0100 Subject: [PATCH 319/412] resolved 2 errors --- src/eynollah/eynollah.py | 6 ++++-- src/eynollah/utils/__init__.py | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 25d5ec4..9158168 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4074,8 +4074,10 @@ class Eynollah: ind_textline_inside_tr = list(range(len(contours[jj]))) index_textline_inside_textregion = index_textline_inside_textregion + ind_textline_inside_tr - ind_ins = [0] * len(contours[jj]) + jj - indexes_of_textline_tot = indexes_of_textline_tot + ind_ins + #ind_ins = [0] * len(contours[jj]) + jj + ind_ins = np.zeros( len(contours[jj]) ) + jj + list_ind_ins = list(ind_ins) + indexes_of_textline_tot = indexes_of_textline_tot + list_ind_ins M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index feab341..a67fc38 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -237,8 +237,11 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order( if len(remained_sep_indexes)>1: #print(np.array(remained_sep_indexes),'np.array(remained_sep_indexes)') #print(np.array(mother),'mother') - remained_sep_indexes_without_mother = remained_sep_indexes[mother==0] - remained_sep_indexes_with_child_without_mother = remained_sep_indexes[mother==0 & child==1] + ##remained_sep_indexes_without_mother = remained_sep_indexes[mother==0] + ##remained_sep_indexes_with_child_without_mother = remained_sep_indexes[mother==0 & child==1] + remained_sep_indexes_without_mother=np.array(list(remained_sep_indexes))[np.array(mother)==0] + remained_sep_indexes_with_child_without_mother=np.array(list(remained_sep_indexes))[(np.array(mother)==0) & (np.array(child)==1)] + #print(remained_sep_indexes_without_mother,'remained_sep_indexes_without_mother') #print(remained_sep_indexes_without_mother,'remained_sep_indexes_without_mother') x_end_with_child_without_mother = x_end[remained_sep_indexes_with_child_without_mother] From 7110bd971f719bd3d86457bd3a1b6375ca952921 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 27 Feb 2025 19:11:15 +0100 Subject: [PATCH 320/412] resolved an error for light version in the case that slope_deskew is smaller than slope_threshold --- src/eynollah/eynollah.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 9158168..6802e47 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4120,7 +4120,7 @@ class Eynollah: def filter_contours_without_textline_inside( self, contours,text_con_org, contours_textline, contours_only_text_parent_d_ordered): - + ###contours_txtline_of_all_textregions = [] ###for jj in range(len(contours_textline)): ###contours_txtline_of_all_textregions = contours_txtline_of_all_textregions + contours_textline[jj] @@ -4156,7 +4156,8 @@ class Eynollah: contours.pop(ind_u_a_trs) contours_textline.pop(ind_u_a_trs) text_con_org.pop(ind_u_a_trs) - contours_only_text_parent_d_ordered.pop(ind_u_a_trs) + if len(contours_only_text_parent_d_ordered) > 0: + contours_only_text_parent_d_ordered.pop(ind_u_a_trs) return contours, text_con_org, contours_textline, contours_only_text_parent_d_ordered, np.array(range(len(contours))) @@ -4518,7 +4519,6 @@ class Eynollah: ###min_con_area = 0.000005 contours_only_text, hir_on_text = return_contours_of_image(text_only) contours_only_text_parent = return_parent_contours(contours_only_text, hir_on_text) - if len(contours_only_text_parent) > 0: areas_cnt_text = np.array([cv2.contourArea(c) for c in contours_only_text_parent]) areas_cnt_text = areas_cnt_text / float(text_only.shape[0] * text_only.shape[1]) @@ -4619,8 +4619,7 @@ class Eynollah: else: contours_only_text_parent_d_ordered = [] contours_only_text_parent_d = [] - contours_only_text_parent = [] - + #contours_only_text_parent = [] if not len(contours_only_text_parent): # stop early empty_marginals = [[]] * len(polygons_of_marginals) @@ -4690,8 +4689,7 @@ class Eynollah: all_found_textline_polygons_marginals) contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, \ index_by_text_par_con = self.filter_contours_without_textline_inside( - contours_only_text_parent, txt_con_org, all_found_textline_polygons, - contours_only_text_parent_d_ordered) + contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered) else: textline_mask_tot_ea = cv2.erode(textline_mask_tot_ea, kernel=KERNEL, iterations=1) all_found_textline_polygons, boxes_text, txt_con_org, contours_only_text_parent, all_box_coord, \ From 687aba1fa288074e3e326e06866cc5acc4beb235 Mon Sep 17 00:00:00 2001 From: Clemens Neudecker <952378+cneud@users.noreply.github.com> Date: Mon, 3 Mar 2025 22:10:40 +0100 Subject: [PATCH 321/412] replace usages of `imutils` with opencv equivalents should fix https://github.com/qurator-spk/eynollah/issues/141 --- src/eynollah/utils/rotate.py | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py index 603c2d9..c01f5e8 100644 --- a/src/eynollah/utils/rotate.py +++ b/src/eynollah/utils/rotate.py @@ -1,6 +1,4 @@ import math - -import imutils import cv2 def rotatedRectWithMaxArea(w, h, angle): @@ -11,11 +9,11 @@ def rotatedRectWithMaxArea(w, h, angle): side_long, side_short = (w, h) if width_is_longer else (h, w) # since the solutions for angle, -angle and 180-angle are all the same, - # if suffices to look at the first quadrant and the absolute values of sin,cos: + # it suffices to look at the first quadrant and the absolute values of sin,cos: sin_a, cos_a = abs(math.sin(angle)), abs(math.cos(angle)) if side_short <= 2.0 * sin_a * cos_a * side_long or abs(sin_a - cos_a) < 1e-10: - # half constrained case: two crop corners touch the longer side, - # the other two corners are on the mid-line parallel to the longer line + # half constrained case: two crop corners touch the longer side, + # the other two corners are on the mid-line parallel to the longer line x = 0.5 * side_short wr, hr = (x / sin_a, x / cos_a) if width_is_longer else (x / cos_a, x / sin_a) else: @@ -25,6 +23,12 @@ def rotatedRectWithMaxArea(w, h, angle): return wr, hr +def rotate_image_opencv(image, angle): + (h, w) = image.shape[:2] + center = (w // 2, h // 2) + M = cv2.getRotationMatrix2D(center, angle, 1.0) + return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) + def rotate_max_area_new(image, rotated, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) h, w, _ = rotated.shape @@ -35,7 +39,7 @@ def rotate_max_area_new(image, rotated, angle): return rotated[y1:y2, x1:x2] def rotation_image_new(img, thetha): - rotated = imutils.rotate(img, thetha) + rotated = rotate_image_opencv(img, thetha) return rotate_max_area_new(img, rotated, thetha) def rotate_image(img_patch, slope): @@ -44,13 +48,10 @@ def rotate_image(img_patch, slope): M = cv2.getRotationMatrix2D(center, slope, 1.0) return cv2.warpAffine(img_patch, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) -def rotate_image_different( img, slope): - # img = cv2.imread('images/input.jpg') +def rotate_image_different(img, slope): num_rows, num_cols = img.shape[:2] - rotation_matrix = cv2.getRotationMatrix2D((num_cols / 2, num_rows / 2), slope, 1) - img_rotation = cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows)) - return img_rotation + return cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows)) def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_table_prediction, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) @@ -62,17 +63,17 @@ def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_ta return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_table_prediction[y1:y2, x1:x2] def rotation_not_90_func(img, textline, text_regions_p_1, table_prediction, thetha): - rotated = imutils.rotate(img, thetha) - rotated_textline = imutils.rotate(textline, thetha) - rotated_layout = imutils.rotate(text_regions_p_1, thetha) - rotated_table_prediction = imutils.rotate(table_prediction, thetha) + rotated = rotate_image_opencv(img, thetha) + rotated_textline = rotate_image_opencv(textline, thetha) + rotated_layout = rotate_image_opencv(text_regions_p_1, thetha) + rotated_table_prediction = rotate_image_opencv(table_prediction, thetha) return rotate_max_area(img, rotated, rotated_textline, rotated_layout, rotated_table_prediction, thetha) def rotation_not_90_func_full_layout(img, textline, text_regions_p_1, text_regions_p_fully, thetha): - rotated = imutils.rotate(img, thetha) - rotated_textline = imutils.rotate(textline, thetha) - rotated_layout = imutils.rotate(text_regions_p_1, thetha) - rotated_layout_full = imutils.rotate(text_regions_p_fully, thetha) + rotated = rotate_image_opencv(img, thetha) + rotated_textline = rotate_image_opencv(textline, thetha) + rotated_layout = rotate_image_opencv(text_regions_p_1, thetha) + rotated_layout_full = rotate_image_opencv(text_regions_p_fully, thetha) return rotate_max_area_full_layout(img, rotated, rotated_textline, rotated_layout, rotated_layout_full, thetha) def rotate_max_area_full_layout(image, rotated, rotated_textline, rotated_layout, rotated_layout_full, angle): @@ -83,4 +84,3 @@ def rotate_max_area_full_layout(image, rotated, rotated_textline, rotated_layout x1 = w // 2 - int(wr / 2) x2 = x1 + int(wr) return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_layout_full[y1:y2, x1:x2] - From 0b2c1b9275077eed0a7963fd4ad2c25624b9b88a Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Mon, 3 Mar 2025 22:21:57 +0100 Subject: [PATCH 322/412] remove `imutils` dependency --- requirements.txt | 1 - src/eynollah/utils/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e6f6e4b..7817f27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 -imutils >= 0.5.3 matplotlib setuptools >= 50 diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index d2b2488..149de6d 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -4,7 +4,6 @@ import matplotlib.pyplot as plt import numpy as np from shapely import geometry import cv2 -import imutils from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d From a4f1f351259a03cd25a4900f52ec2ff1f4cef3c1 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 7 Mar 2025 13:19:56 +0100 Subject: [PATCH 323/412] Resolving test failure --- src/eynollah/eynollah.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index e1d3895..9cce812 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1899,7 +1899,7 @@ class Eynollah: ##self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light) if num_col_classifier == 1 or num_col_classifier == 2: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) + model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: self.logger.debug("resized to %dx%d for %d cols", img_resized.shape[1], img_resized.shape[0], num_col_classifier) From aa72ca3006288ea5dc6e05ec987cb44b3d234687 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 13 Mar 2025 15:02:38 +0100 Subject: [PATCH 324/412] Resolved an issue in the OCR-D framework where dir_out received a None value --- src/eynollah/processor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index ed510c8..4eced21 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -45,6 +45,7 @@ class EynollahProcessor(Processor): image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(local_filename=page.imageFilename))).local_filename eynollah_kwargs = { 'dir_models': self.resolve_resource(self.parameter['models']), + 'dir_out': self.output_file_grp, 'allow_enhancement': False, 'curved_line': self.parameter['curved_line'], 'full_layout': self.parameter['full_layout'], From c8b85299514e900707f8e0f6bdc966d1c4a191cf Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 17 Mar 2025 19:50:58 +0100 Subject: [PATCH 325/412] For the CNN-RNN OCR model, long text lines are split into two segments --- src/eynollah/eynollah.py | 74 +++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 9cce812..65e85c5 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4961,7 +4961,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_1_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_1_new_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5080,6 +5080,18 @@ class Eynollah_ocr: return [image1, image2] else: return None + def preprocess_and_resize_image_for_ocrcnn_model(self, img, image_height, image_width): + ratio = image_height /float(img.shape[0]) + w_ratio = int(ratio * img.shape[1]) + if w_ratio <= image_width: + width_new = w_ratio + else: + width_new = image_width + img = resize_image(img, image_height, width_new) + img_fin = np.ones((image_height, image_width, 3))*255 + img_fin[:,:width_new,:] = img[:,:,:] + img_fin = img_fin / 255. + return img_fin def run(self): ls_imgs = os.listdir(self.dir_in) @@ -5214,7 +5226,7 @@ class Eynollah_ocr: else: max_len = 512 padding_token = 299 - image_width = max_len * 4 + image_width = 512#max_len * 4 image_height = 32 b_s = 8 @@ -5265,10 +5277,31 @@ class Eynollah_ocr: mask_poly = mask_poly[y:y+h, x:x+w, :] img_crop = img_poly_on_img[y:y+h, x:x+w, :] img_crop[mask_poly==0] = 255 - img_crop = tf.reverse(img_crop,axis=[-1]) - img_crop = self.distortion_free_resize(img_crop, img_size) - img_crop = tf.cast(img_crop, tf.float32) / 255.0 - cropped_lines.append(img_crop) + + if h2w_ratio > 0.05: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(0) + else: + splited_images = self.return_textlines_split_if_needed(img_crop) + #print(splited_images) + if splited_images: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[0], image_height, image_width) + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(1) + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[1], image_height, image_width) + + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(-1) + else: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(0) + #img_crop = tf.reverse(img_crop,axis=[-1]) + #img_crop = self.distortion_free_resize(img_crop, img_size) + #img_crop = tf.cast(img_crop, tf.float32) / 255.0 + + #cropped_lines.append(img_crop) indexer_text_region = indexer_text_region +1 @@ -5282,12 +5315,12 @@ class Eynollah_ocr: n_start = i*b_s imgs = cropped_lines[n_start:] imgs = np.array(imgs) - imgs = imgs.reshape(imgs.shape[0], image_width, image_height, 3) + imgs = imgs.reshape(imgs.shape[0], image_height, image_width, 3) else: n_start = i*b_s n_end = (i+1)*b_s imgs = cropped_lines[n_start:n_end] - imgs = np.array(imgs).reshape(b_s, image_width, image_height, 3) + imgs = np.array(imgs).reshape(b_s, image_height, image_width, 3) preds = self.prediction_model.predict(imgs, verbose=0) @@ -5296,15 +5329,32 @@ class Eynollah_ocr: for ib in range(imgs.shape[0]): pred_texts_ib = pred_texts[ib].strip("[UNK]") extracted_texts.append(pred_texts_ib) - + + + extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] + + extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] + #print(extracted_texts_merged, len(extracted_texts_merged)) + unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) - + + #print(len(unique_cropped_lines_region_indexer), 'unique_cropped_lines_region_indexer') text_by_textregion = [] for ind in unique_cropped_lines_region_indexer: - extracted_texts_merged_un = np.array(extracted_texts)[np.array(cropped_lines_region_indexer)==ind] + extracted_texts_merged_un = np.array(extracted_texts_merged)[np.array(cropped_lines_region_indexer)==ind] text_by_textregion.append(" ".join(extracted_texts_merged_un)) + + + ##unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) + + ##text_by_textregion = [] + ##for ind in unique_cropped_lines_region_indexer: + ##extracted_texts_merged_un = np.array(extracted_texts)[np.array(cropped_lines_region_indexer)==ind] + + ##text_by_textregion.append(" ".join(extracted_texts_merged_un)) + indexer = 0 indexer_textregion = 0 for nn in root1.iter(region_tags): @@ -5317,7 +5367,7 @@ class Eynollah_ocr: if child_textregion.tag.endswith("TextLine"): text_subelement = ET.SubElement(child_textregion, 'TextEquiv') unicode_textline = ET.SubElement(text_subelement, 'Unicode') - unicode_textline.text = extracted_texts[indexer] + unicode_textline.text = extracted_texts_merged[indexer] indexer = indexer + 1 has_textline = True if has_textline: From d3a4c06e7f5aaafae356d228df6bf54851bc7b82 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 20 Mar 2025 18:21:44 +0100 Subject: [PATCH 326/412] This commit enables the export of cropped text line images along with their corresponding texts from a Page-XML file. These exported text line images and texts can be utilized for training a text line-based OCR model. --- src/eynollah/cli.py | 16 +++- src/eynollah/eynollah.py | 188 ++++++++++++++++++++------------------- 2 files changed, 110 insertions(+), 94 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index ff612b2..c306ac5 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -347,6 +347,18 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ is_flag=True, help="if this parameter set to true, transformer ocr will be applied, otherwise cnn_rnn model.", ) +@click.option( + "--export_textline_images_and_text", + "-etit/-noetit", + is_flag=True, + help="if this parameter set to true, images and text in xml will be exported into output dir. This files can be used for training a OCR engine.", +) +@click.option( + "--do_not_mask_with_textline_contour", + "-nmtc/-mtc", + is_flag=True, + help="if this parameter set to true, cropped textline images will not be masked with textline contour.", +) @click.option( "--log_level", "-l", @@ -354,7 +366,7 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ help="Override log level globally to this", ) -def ocr(dir_in, out, dir_xmls, model, tr_ocr, log_level): +def ocr(dir_in, out, dir_xmls, model, tr_ocr, export_textline_images_and_text, do_not_mask_with_textline_contour, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() @@ -364,6 +376,8 @@ def ocr(dir_in, out, dir_xmls, model, tr_ocr, log_level): dir_out=out, dir_models=model, tr_ocr=tr_ocr, + export_textline_images_and_text=export_textline_images_and_text, + do_not_mask_with_textline_contour=do_not_mask_with_textline_contour, ) eynollah_ocr.run() diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 65e85c5..7acee39 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4946,6 +4946,8 @@ class Eynollah_ocr: dir_in=None, dir_out=None, tr_ocr=False, + export_textline_images_and_text=False, + do_not_mask_with_textline_contour=False, logger=None, ): self.dir_in = dir_in @@ -4953,6 +4955,8 @@ class Eynollah_ocr: self.dir_xmls = dir_xmls self.dir_models = dir_models self.tr_ocr = tr_ocr + self.export_textline_images_and_text = export_textline_images_and_text + self.do_not_mask_with_textline_contour = do_not_mask_with_textline_contour if tr_ocr: self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") @@ -4961,7 +4965,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_1_new_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_3_new_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5107,7 +5111,7 @@ class Eynollah_ocr: img = cv2.imread(dir_img) ##file_name = Path(dir_xmls).stem - tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding = 'iso-8859-5')) + tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding="utf-8")) root1=tree1.getroot() alltags=[elem.tag for elem in root1.iter()] link=alltags[0].split('}')[0]+'}' @@ -5241,7 +5245,7 @@ class Eynollah_ocr: out_file_ocr = os.path.join(self.dir_out, file_name+'.xml') img = cv2.imread(dir_img) - tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding = 'iso-8859-5')) + tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding="utf-8")) root1=tree1.getroot() alltags=[elem.tag for elem in root1.iter()] link=alltags[0].split('}')[0]+'}' @@ -5257,15 +5261,16 @@ class Eynollah_ocr: tinl = time.time() indexer_text_region = 0 + indexer_textlines = 0 for nn in root1.iter(region_tags): for child_textregion in nn: if child_textregion.tag.endswith("TextLine"): - for child_textlines in child_textregion: if child_textlines.tag.endswith("Coords"): cropped_lines_region_indexer.append(indexer_text_region) p_h=child_textlines.attrib['points'].split(' ') textline_coords = np.array( [ [ int(x.split(',')[0]) , int(x.split(',')[1]) ] for x in p_h] ) + x,y,w,h = cv2.boundingRect(textline_coords) h2w_ratio = h/float(w) @@ -5276,104 +5281,101 @@ class Eynollah_ocr: mask_poly = mask_poly[y:y+h, x:x+w, :] img_crop = img_poly_on_img[y:y+h, x:x+w, :] - img_crop[mask_poly==0] = 255 + if not self.do_not_mask_with_textline_contour: + img_crop[mask_poly==0] = 255 - if h2w_ratio > 0.05: - img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) - cropped_lines.append(img_fin) - cropped_lines_meging_indexing.append(0) - else: - splited_images = self.return_textlines_split_if_needed(img_crop) - #print(splited_images) - if splited_images: - img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[0], image_height, image_width) - cropped_lines.append(img_fin) - cropped_lines_meging_indexing.append(1) - img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[1], image_height, image_width) - - cropped_lines.append(img_fin) - cropped_lines_meging_indexing.append(-1) - else: + if not self.export_textline_images_and_text: + if h2w_ratio > 0.05: img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) cropped_lines.append(img_fin) cropped_lines_meging_indexing.append(0) - #img_crop = tf.reverse(img_crop,axis=[-1]) - #img_crop = self.distortion_free_resize(img_crop, img_size) - #img_crop = tf.cast(img_crop, tf.float32) / 255.0 - - #cropped_lines.append(img_crop) + else: + splited_images = self.return_textlines_split_if_needed(img_crop) + if splited_images: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[0], image_height, image_width) + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(1) + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[1], image_height, image_width) + + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(-1) + else: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) + cropped_lines.append(img_fin) + cropped_lines_meging_indexing.append(0) + + if self.export_textline_images_and_text: + if child_textlines.tag.endswith("TextEquiv"): + for cheild_text in child_textlines: + if cheild_text.tag.endswith("Unicode"): + textline_text = cheild_text.text + if not textline_text: + pass + else: + with open(os.path.join(self.dir_out, file_name+'_line_'+str(indexer_textlines)+'.txt'), 'w') as text_file: + text_file.write(textline_text) - indexer_text_region = indexer_text_region +1 + cv2.imwrite(os.path.join(self.dir_out, file_name+'_line_'+str(indexer_textlines)+'.png'), img_crop ) + + indexer_textlines+=1 + + if not self.export_textline_images_and_text: + indexer_text_region = indexer_text_region +1 - - extracted_texts = [] + if not self.export_textline_images_and_text: + extracted_texts = [] - n_iterations = math.ceil(len(cropped_lines) / b_s) + n_iterations = math.ceil(len(cropped_lines) / b_s) - for i in range(n_iterations): - if i==(n_iterations-1): - n_start = i*b_s - imgs = cropped_lines[n_start:] - imgs = np.array(imgs) - imgs = imgs.reshape(imgs.shape[0], image_height, image_width, 3) - else: - n_start = i*b_s - n_end = (i+1)*b_s - imgs = cropped_lines[n_start:n_end] - imgs = np.array(imgs).reshape(b_s, image_height, image_width, 3) + for i in range(n_iterations): + if i==(n_iterations-1): + n_start = i*b_s + imgs = cropped_lines[n_start:] + imgs = np.array(imgs) + imgs = imgs.reshape(imgs.shape[0], image_height, image_width, 3) + else: + n_start = i*b_s + n_end = (i+1)*b_s + imgs = cropped_lines[n_start:n_end] + imgs = np.array(imgs).reshape(b_s, image_height, image_width, 3) + + + preds = self.prediction_model.predict(imgs, verbose=0) + pred_texts = self.decode_batch_predictions(preds) + + for ib in range(imgs.shape[0]): + pred_texts_ib = pred_texts[ib].strip("[UNK]") + extracted_texts.append(pred_texts_ib) + + extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] + + extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] + unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) + + text_by_textregion = [] + for ind in unique_cropped_lines_region_indexer: + extracted_texts_merged_un = np.array(extracted_texts_merged)[np.array(cropped_lines_region_indexer)==ind] + text_by_textregion.append(" ".join(extracted_texts_merged_un)) + indexer = 0 + indexer_textregion = 0 + for nn in root1.iter(region_tags): + text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') + unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') - preds = self.prediction_model.predict(imgs, verbose=0) - pred_texts = self.decode_batch_predictions(preds) - - for ib in range(imgs.shape[0]): - pred_texts_ib = pred_texts[ib].strip("[UNK]") - extracted_texts.append(pred_texts_ib) - - extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] + has_textline = False + for child_textregion in nn: + if child_textregion.tag.endswith("TextLine"): + text_subelement = ET.SubElement(child_textregion, 'TextEquiv') + unicode_textline = ET.SubElement(text_subelement, 'Unicode') + unicode_textline.text = extracted_texts_merged[indexer] + indexer = indexer + 1 + has_textline = True + if has_textline: + unicode_textregion.text = text_by_textregion[indexer_textregion] + indexer_textregion = indexer_textregion + 1 - extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] - #print(extracted_texts_merged, len(extracted_texts_merged)) - - unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) - - #print(len(unique_cropped_lines_region_indexer), 'unique_cropped_lines_region_indexer') - text_by_textregion = [] - for ind in unique_cropped_lines_region_indexer: - extracted_texts_merged_un = np.array(extracted_texts_merged)[np.array(cropped_lines_region_indexer)==ind] - - text_by_textregion.append(" ".join(extracted_texts_merged_un)) - - - - ##unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) - - ##text_by_textregion = [] - ##for ind in unique_cropped_lines_region_indexer: - ##extracted_texts_merged_un = np.array(extracted_texts)[np.array(cropped_lines_region_indexer)==ind] - - ##text_by_textregion.append(" ".join(extracted_texts_merged_un)) - - indexer = 0 - indexer_textregion = 0 - for nn in root1.iter(region_tags): - text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') - unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') - - - has_textline = False - for child_textregion in nn: - if child_textregion.tag.endswith("TextLine"): - text_subelement = ET.SubElement(child_textregion, 'TextEquiv') - unicode_textline = ET.SubElement(text_subelement, 'Unicode') - unicode_textline.text = extracted_texts_merged[indexer] - indexer = indexer + 1 - has_textline = True - if has_textline: - unicode_textregion.text = text_by_textregion[indexer_textregion] - indexer_textregion = indexer_textregion + 1 - - ET.register_namespace("",name_space) - tree1.write(out_file_ocr,xml_declaration=True,method='xml',encoding="utf8",default_namespace=None) - #print("Job done in %.1fs", time.time() - t0) + ET.register_namespace("",name_space) + tree1.write(out_file_ocr,xml_declaration=True,method='xml',encoding="utf8",default_namespace=None) + #print("Job done in %.1fs", time.time() - t0) From 370d44a66b8bbca23eacec0521dd3e68138638bd Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 26 Mar 2025 10:45:34 +0100 Subject: [PATCH 327/412] Slope deskew in the light version is set to zero because when the slope_deskew value exceeds the slope_threshold, the reading order becomes incorrect. This issue needs to be addressed. Additionally, the textlines order within text region in the light version was reversed, and this has been corrected. --- src/eynollah/eynollah.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 7acee39..fd3eb25 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -1575,7 +1575,7 @@ class Eynollah: indexes_in = args_textlines[results==1] textlines_ins = [polygons_of_textlines[ind] for ind in indexes_in] - all_found_textline_polygons.append(textlines_ins) + all_found_textline_polygons.append(textlines_ins[::-1]) slopes.append(slope_deskew) _, crop_coor = crop_image_inside_box(boxes[index],image_page_rotated) @@ -4417,9 +4417,9 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = 0, 0 #self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) + slope_deskew, slope_first = 0, 0 #self.run_deskew(textline_mask_tot_ea) #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, \ @@ -4965,7 +4965,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_3_new_ocrcnn"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_step_100000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5309,9 +5309,7 @@ class Eynollah_ocr: for cheild_text in child_textlines: if cheild_text.tag.endswith("Unicode"): textline_text = cheild_text.text - if not textline_text: - pass - else: + if textline_text: with open(os.path.join(self.dir_out, file_name+'_line_'+str(indexer_textlines)+'.txt'), 'w') as text_file: text_file.write(textline_text) From 7df0427b0479bdfb00cc789e09ae3ecb08cc9bb7 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Wed, 26 Mar 2025 18:42:06 +0100 Subject: [PATCH 328/412] In the context of OCR, if Page-XML files already contain text, the new predicted text will replace the existing text. --- src/eynollah/eynollah.py | 43 +++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index fd3eb25..7cbab6a 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4965,7 +4965,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_step_100000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_step_150000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5358,20 +5358,49 @@ class Eynollah_ocr: indexer = 0 indexer_textregion = 0 for nn in root1.iter(region_tags): - text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') - unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') + + is_textregion_text = False + for childtest in nn: + if childtest.tag.endswith("TextEquiv"): + is_textregion_text = True + + if not is_textregion_text: + text_subelement_textregion = ET.SubElement(nn, 'TextEquiv') + unicode_textregion = ET.SubElement(text_subelement_textregion, 'Unicode') has_textline = False for child_textregion in nn: if child_textregion.tag.endswith("TextLine"): - text_subelement = ET.SubElement(child_textregion, 'TextEquiv') - unicode_textline = ET.SubElement(text_subelement, 'Unicode') - unicode_textline.text = extracted_texts_merged[indexer] + + is_textline_text = False + for childtest2 in child_textregion: + if childtest2.tag.endswith("TextEquiv"): + is_textline_text = True + + + if not is_textline_text: + text_subelement = ET.SubElement(child_textregion, 'TextEquiv') + unicode_textline = ET.SubElement(text_subelement, 'Unicode') + unicode_textline.text = extracted_texts_merged[indexer] + else: + for childtest3 in child_textregion: + if childtest3.tag.endswith("TextEquiv"): + for child_uc in childtest3: + if child_uc.tag.endswith("Unicode"): + child_uc.text = extracted_texts_merged[indexer] + indexer = indexer + 1 has_textline = True if has_textline: - unicode_textregion.text = text_by_textregion[indexer_textregion] + if is_textregion_text: + for child4 in nn: + if child4.tag.endswith("TextEquiv"): + for childtr_uc in child4: + if childtr_uc.tag.endswith("Unicode"): + childtr_uc.text = text_by_textregion[indexer_textregion] + else: + unicode_textregion.text = text_by_textregion[indexer_textregion] indexer_textregion = indexer_textregion + 1 ET.register_namespace("",name_space) From 181c0c584f0370c789557b8db0610636bed414fb Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:25:22 +0100 Subject: [PATCH 329/412] bbox rotation with opencv --- src/eynollah/utils/rotate.py | 42 ++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py index c01f5e8..734f924 100644 --- a/src/eynollah/utils/rotate.py +++ b/src/eynollah/utils/rotate.py @@ -1,4 +1,5 @@ import math +import numpy as np import cv2 def rotatedRectWithMaxArea(w, h, angle): @@ -23,11 +24,44 @@ def rotatedRectWithMaxArea(w, h, angle): return wr, hr + def rotate_image_opencv(image, angle): - (h, w) = image.shape[:2] - center = (w // 2, h // 2) - M = cv2.getRotationMatrix2D(center, angle, 1.0) - return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) + # Calculate the original image dimensions (h, w) and the center point (cx, cy) + h, w = image.shape[:2] + cx, cy = (w // 2, h // 2) + + # Compute the rotation matrix + M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0) + + # Calculate the new bounding box + corners = np.array([ + [0, 0], + [w, 0], + [w, h], + [0, h] + ]) + + # Apply rotation matrix to the corner points + ones = np.ones(shape=(len(corners), 1)) + corners_ones = np.hstack([corners, ones]) + transformed_corners = M @ corners_ones.T + transformed_corners = transformed_corners.T + + # Calculate the new bounding box dimensions + min_x, min_y = np.min(transformed_corners, axis=0) + max_x, max_y = np.max(transformed_corners, axis=0) + + newW = int(np.ceil(max_x - min_x)) + newH = int(np.ceil(max_y - min_y)) + + # Adjust the rotation matrix to account for translation + M[0, 2] += (newW / 2) - cx + M[1, 2] += (newH / 2) - cy + + # Perform the affine transformation (rotation) + rotated_image = cv2.warpAffine(image, M, (newW, newH)) + + return rotated_image def rotate_max_area_new(image, rotated, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) From 6f36c7177f0b1c9d9ad5cf398f0211a8f07a8f5b Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Thu, 27 Mar 2025 18:24:47 +0100 Subject: [PATCH 330/412] For OCR, the splitting ratio of text lines is adjusted --- src/eynollah/eynollah.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 7cbab6a..34fc8cb 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -5091,6 +5091,7 @@ class Eynollah_ocr: width_new = w_ratio else: width_new = image_width + img = resize_image(img, image_height, width_new) img_fin = np.ones((image_height, image_width, 3))*255 img_fin[:,:width_new,:] = img[:,:,:] @@ -5285,7 +5286,7 @@ class Eynollah_ocr: img_crop[mask_poly==0] = 255 if not self.export_textline_images_and_text: - if h2w_ratio > 0.05: + if h2w_ratio > 0.1: img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) cropped_lines.append(img_fin) cropped_lines_meging_indexing.append(0) @@ -5345,7 +5346,7 @@ class Eynollah_ocr: pred_texts_ib = pred_texts[ib].strip("[UNK]") extracted_texts.append(pred_texts_ib) - extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] + extracted_texts_merged = [extracted_texts[ind] if cropped_lines_meging_indexing[ind]==0 else extracted_texts[ind]+" "+extracted_texts[ind+1] if cropped_lines_meging_indexing[ind]==1 else None for ind in range(len(cropped_lines_meging_indexing))] extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) From e9fa6913081f16f6bd7df3238b91437f230ab785 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:41:10 +0100 Subject: [PATCH 331/412] add model and training documentation --- README.md | 17 +- docs/models.md | 145 ++++++++++++ docs/train.md | 632 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 783 insertions(+), 11 deletions(-) create mode 100644 docs/models.md create mode 100644 docs/train.md diff --git a/README.md b/README.md index 916c556..5699948 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ Alternatively, you can run `make install` or `make install-dev` for editable ins ## Models Pre-trained models can be downloaded from [qurator-data.de](https://qurator-data.de/eynollah/) or [huggingface](https://huggingface.co/SBB?search_models=eynollah). +For documentation on methods and models, have a look at [`models.md`](https://github.com/qurator-spk/eynollah/tree/main/docs/models.md). + ## Train -🚧 **Work in progress** - -In case you want to train your own model, have a look at [`sbb_pixelwise_segmentation`](https://github.com/qurator-spk/sbb_pixelwise_segmentation). +In case you want to train your own model with Eynollah, have a look at [`train.md`](https://github.com/qurator-spk/eynollah/tree/main/docs/train.md). ## Usage The command-line interface can be called like this: @@ -83,11 +83,9 @@ If no option is set, the tool performs layout detection of main regions (backgro The best output quality is produced when RGB images are used as input rather than greyscale or binarized images. #### Use as OCR-D processor -🚧 **Work in progress** +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor that is described in [`ocrd-tool.json`](https://github.com/qurator-spk/eynollah/tree/main/src/eynollah/ocrd-tool.json). -Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor. - -In this case, the source image file group with (preferably) RGB images should be used as input like this: +The source image file group with (preferably) RGB images should be used as input for Eynollah like this: ``` ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models @@ -99,10 +97,7 @@ Any image referenced by `@imageFilename` in PAGE-XML is passed on directly to Ey ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models ``` -uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps - -#### Additional documentation -Please check the [wiki](https://github.com/qurator-spk/eynollah/wiki). +uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps. ## How to cite If you find this tool useful in your work, please consider citing our paper: diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..c6f7340 --- /dev/null +++ b/docs/models.md @@ -0,0 +1,145 @@ +# Models documentation +This suite of 14 models presents a document layout analysis (DLA) system for historical documents implemented by +pixel-wise segmentation using a combination of a ResNet50 encoder with various U-Net decoders. In addition, heuristic +methods are applied to detect marginals and to determine the reading order of text regions. + +The detection and classification of multiple classes of layout elements such as headings, images, tables etc. as part of +DLA is required in order to extract and process them in subsequent steps. Altogether, the combination of image +detection, classification and segmentation on the wide variety that can be found in over 400 years of printed cultural +heritage makes this a very challenging task. Deep learning models are complemented with heuristics for the detection of +text lines, marginals, and reading order. Furthermore, an optional image enhancement step was added in case of documents +that either have insufficient pixel density and/or require scaling. Also, a column classifier for the analysis of +multi-column documents was added. With these additions, DLA performance was improved, and a high accuracy in the +prediction of the reading order is accomplished. + +Two Arabic/Persian terms form the name of the model suite: عين الله, which can be transcribed as "ain'allah" or +"eynollah"; it translates into English as "God's Eye" -- it sees (nearly) everything on the document image. + +See the flowchart below for the different stages and how they interact: +![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) + +## Models + +### Image enhancement +Model card: [Image Enhancement](https://huggingface.co/SBB/eynollah-enhancement) + +This model addresses image resolution, specifically targeting documents with suboptimal resolution. In instances where +the detection of document layout exhibits inadequate performance, the proposed enhancement aims to significantly improve +the quality and clarity of the images, thus facilitating enhanced visual interpretation and analysis. + +### Page extraction / border detection +Model card: [Page Extraction/Border Detection](https://huggingface.co/SBB/eynollah-page-extraction) + +A problem that can negatively affect OCR are black margins around a page caused by document scanning. A deep learning +model helps to crop to the page borders by using a pixel-wise segmentation method. + +### Column classification +Model card: [Column Classification](https://huggingface.co/SBB/eynollah-column-classifier) + +This model is a trained classifier that recognizes the number of columns in a document by use of a training set with +manual classification of all documents into six classes with either one, two, three, four, five, or six and more columns +respectively. + +### Binarization +Model card: [Binarization](https://huggingface.co/SBB/eynollah-binarization) + +This model is designed to tackle the intricate task of document image binarization, which involves segmentation of the +image into white and black pixels. This process significantly contributes to the overall performance of the layout +models, particularly in scenarios where the documents are degraded or exhibit subpar quality. The robust binarization +capability of the model enables improved accuracy and reliability in subsequent layout analysis, thereby facilitating +enhanced document understanding and interpretation. + +### Main region detection +Model card: [Main Region Detection](https://huggingface.co/SBB/eynollah-main-regions) + +This model has employed a different set of labels, including an artificial class specifically designed to encompass the +text regions. The inclusion of this artificial class facilitates easier isolation of text regions by the model. This +approach grants the advantage of training the model using downscaled images, which in turn leads to faster predictions +during the inference phase. By incorporating this methodology, improved efficiency is achieved without compromising the +model's ability to accurately identify and classify text regions within documents. + +### Main region detection (with scaling augmentation) +Model card: [Main Region Detection (with scaling augmentation)](https://huggingface.co/SBB/eynollah-main-regions-aug-scaling) + +Utilizing scaling augmentation, this model leverages the capability to effectively segment elements of extremely high or +low scales within documents. By harnessing this technique, the tool gains a significant advantage in accurately +categorizing and isolating such elements, thereby enhancing its overall performance and enabling precise analysis of +documents with varying scale characteristics. + +### Main region detection (with rotation augmentation) +Model card: [Main Region Detection (with rotation augmentation)](https://huggingface.co/SBB/eynollah-main-regions-aug-rotation) + +This model takes advantage of rotation augmentation. This helps the tool to segment the vertical text regions in a +robust way. + +### Main region detection (ensembled) +Model card: [Main Region Detection (ensembled)](https://huggingface.co/SBB/eynollah-main-regions-ensembled) + +The robustness of this model is attained through an ensembling technique that combines the weights from various epochs. +By employing this approach, the model achieves a high level of resilience and stability, effectively leveraging the +strengths of multiple epochs to enhance its overall performance and deliver consistent and reliable results. + +### Full region detection (1,2-column documents) +Model card: [Full Region Detection (1,2-column documents)](https://huggingface.co/SBB/eynollah-full-regions-1column) + +This model deals with documents comprising of one and two columns. + +### Full region detection (3,n-column documents) +Model card: [Full Region Detection (3,n-column documents)](https://huggingface.co/SBB/eynollah-full-regions-3pluscolumn) + +This model is responsible for detecting headers and drop capitals in documents with three or more columns. + +### Textline detection +Model card: [Textline Detection](https://huggingface.co/SBB/eynollah-textline) + +The method for textline detection combines deep learning and heuristics. In the deep learning part, an image-to-image +model performs binary segmentation of the document into the classes textline vs. background. In the heuristics part, +bounding boxes or contours are derived from binary segmentation. + +Skewed documents can heavily affect textline detection accuracy, so robust deskewing is needed. But detecting textlines +with rectangle bounding boxes cannot deal with partially curved textlines. To address this, a functionality +specifically for documents with curved textlines was included. After finding the contour of a text region and its +corresponding textline segmentation, the text region is cut into smaller vertical straps. For each strap, its textline +segmentation is first deskewed and then the textlines are separated with the same heuristic method as for finding +textline bounding boxes. Later, the strap is rotated back into its original orientation. + +### Textline detection (light) +Model card: [Textline Detection Light (simpler but faster method)](https://huggingface.co/SBB/eynollah-textline_light) + +The method for textline detection combines deep learning and heuristics. In the deep learning part, an image-to-image +model performs binary segmentation of the document into the classes textline vs. background. In the heuristics part, +bounding boxes or contours are derived from binary segmentation. + +In the context of this textline model, a distinct labeling approach has been employed to ensure accurate predictions. +Specifically, an artificial bounding class has been incorporated alongside the textline classes. This strategic +inclusion effectively prevents any spurious connections between adjacent textlines during the prediction phase, thereby +enhancing the model's ability to accurately identify and delineate individual textlines within documents. This model +eliminates the need for additional heuristics in extracting textline contours. + +### Table detection +Model card: [Table Detection](https://huggingface.co/SBB/eynollah-tables) + +The objective of this model is to perform table segmentation in historical document images. Due to the pixel-wise +segmentation approach employed and the presence of traditional tables predominantly composed of text, the detection of +tables required the incorporation of heuristics to achieve reasonable performance. These heuristics were necessary to +effectively identify and delineate tables within the historical document images, ensuring accurate segmentation and +enabling subsequent analysis and interpretation. + +### Image detection +Model card: [Image Detection](https://huggingface.co/SBB/eynollah-image-extraction) + +This model is used for the task of illustration detection only. + +### Reading order detection +Model card: [Reading Order Detection]() + +TODO + +## Heuristic methods +Additionally, some heuristic methods are employed to further improve the model predictions: +* After border detection, the largest contour is determined by a bounding box, and the image cropped to these coordinates. +* For text region detection, the image is scaled up to make it easier for the model to detect background space between text regions. +* A minimum area is defined for text regions in relation to the overall image dimensions, so that very small regions that are noise can be filtered out. +* Deskewing is applied on the text region level (due to regions having different degrees of skew) in order to improve the textline segmentation result. +* After deskewing, a calculation of the pixel distribution on the X-axis allows the separation of textlines (foreground) and background pixels. +* Finally, using the derived coordinates, bounding boxes are determined for each textline. diff --git a/docs/train.md b/docs/train.md new file mode 100644 index 0000000..9f44a63 --- /dev/null +++ b/docs/train.md @@ -0,0 +1,632 @@ +# Training documentation +This aims to assist users in preparing training datasets, training models, and performing inference with trained models. +We cover various use cases including pixel-wise segmentation, image classification, image enhancement, and machine-based +reading order detection. For each use case, we provide guidance on how to generate the corresponding training dataset. + +The following three tasks can all be accomplished using the code in the +[`train`](https://github.com/qurator-spk/sbb_pixelwise_segmentation/tree/unifying-training-models) directory: + +* generate training dataset +* train a model +* inference with the trained model + +## Generate training dataset +The script `generate_gt_for_training.py` is used for generating training datasets. As the results of the following +command demonstrates, the dataset generator provides three different commands: + +`python generate_gt_for_training.py --help` + +These three commands are: + +* image-enhancement +* machine-based-reading-order +* pagexml2label + +### image-enhancement +Generating a training dataset for image enhancement is quite straightforward. All that is needed is a set of +high-resolution images. The training dataset can then be generated using the following command: + +`python generate_gt_for_training.py image-enhancement -dis "dir of high resolution images" -dois "dir where degraded +images will be written" -dols "dir where the corresponding high resolution image will be written as label" -scs +"degrading scales json file"` + +The scales JSON file is a dictionary with a key named 'scales' and values representing scales smaller than 1. Images are +downscaled based on these scales and then upscaled again to their original size. This process causes the images to lose +resolution at different scales. The degraded images are used as input images, and the original high-resolution images +serve as labels. The enhancement model can be trained with this generated dataset. The scales JSON file looks like this: + +```yaml +{ + "scales": [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9] +} +``` + +### machine-based-reading-order +For machine-based reading order, we aim to determine the reading priority between two sets of text regions. The model's +input is a three-channel image: the first and last channels contain information about each of the two text regions, +while the middle channel encodes prominent layout elements necessary for reading order, such as separators and headers. +To generate the training dataset, our script requires a page XML file that specifies the image layout with the correct +reading order. + +For output images, it is necessary to specify the width and height. Additionally, a minimum text region size can be set +to filter out regions smaller than this minimum size. This minimum size is defined as the ratio of the text region area +to the image area, with a default value of zero. To run the dataset generator, use the following command: + +`python generate_gt_for_training.py machine-based-reading-order -dx "dir of GT xml files" -domi "dir where output images +will be written" -docl "dir where the labels will be written" -ih "height" -iw "width" -min "min area ratio"` + +### pagexml2label +pagexml2label is designed to generate labels from GT page XML files for various pixel-wise segmentation use cases, +including 'layout,' 'textline,' 'printspace,' 'glyph,' and 'word' segmentation. +To train a pixel-wise segmentation model, we require images along with their corresponding labels. Our training script +expects a PNG image where each pixel corresponds to a label, represented by an integer. The background is always labeled +as zero, while other elements are assigned different integers. For instance, if we have ground truth data with four +elements including the background, the classes would be labeled as 0, 1, 2, and 3 respectively. + +In binary segmentation scenarios such as textline or page extraction, the background is encoded as 0, and the desired +element is automatically encoded as 1 in the PNG label. + +To specify the desired use case and the elements to be extracted in the PNG labels, a custom JSON file can be passed. +For example, in the case of 'textline' detection, the JSON file would resemble this: + +```yaml +{ +"use_case": "textline" +} +``` + +In the case of layout segmentation a custom config json file can look like this: + +```yaml +{ +"use_case": "layout", +"textregions":{"rest_as_paragraph":1 , "drop-capital": 1, "header":2, "heading":2, "marginalia":3}, +"imageregion":4, +"separatorregion":5, +"graphicregions" :{"rest_as_decoration":6 ,"stamp":7} +} +``` + +A possible custom config json file for layout segmentation where the "printspace" is a class: + +```yaml +{ +"use_case": "layout", +"textregions":{"rest_as_paragraph":1 , "drop-capital": 1, "header":2, "heading":2, "marginalia":3}, +"imageregion":4, +"separatorregion":5, +"graphicregions" :{"rest_as_decoration":6 ,"stamp":7} +"printspace_as_class_in_layout" : 8 +} +``` + +For the layout use case, it is beneficial to first understand the structure of the page XML file and its elements. +In a given image, the annotations of elements are recorded in a page XML file, including their contours and classes. +For an image document, the known regions are 'textregion', 'separatorregion', 'imageregion', 'graphicregion', +'noiseregion', and 'tableregion'. + +Text regions and graphic regions also have their own specific types. The known types for text regions are 'paragraph', +'header', 'heading', 'marginalia', 'drop-capital', 'footnote', 'footnote-continued', 'signature-mark', 'page-number', +and 'catch-word'. The known types for graphic regions are 'handwritten-annotation', 'decoration', 'stamp', and +'signature'. +Since we don't know all types of text and graphic regions, unknown cases can arise. To handle these, we have defined +two additional types, "rest_as_paragraph" and "rest_as_decoration", to ensure that no unknown types are missed. +This way, users can extract all known types from the labels and be confident that no unknown types are overlooked. + +In the custom JSON file shown above, "header" and "heading" are extracted as the same class, while "marginalia" is shown +as a different class. All other text region types, including "drop-capital," are grouped into the same class. For the +graphic region, "stamp" has its own class, while all other types are classified together. "Image region" and "separator +region" are also present in the label. However, other regions like "noise region" and "table region" will not be +included in the label PNG file, even if they have information in the page XML files, as we chose not to include them. + +`python generate_gt_for_training.py pagexml2label -dx "dir of GT xml files" -do "dir where output label png files will +be written" -cfg "custom config json file" -to "output type which has 2d and 3d. 2d is used for training and 3d is just +to visualise the labels" "` + +We have also defined an artificial class that can be added to the boundary of text region types or text lines. This key +is called "artificial_class_on_boundary." If users want to apply this to certain text regions in the layout use case, +the example JSON config file should look like this: + +```yaml +{ + "use_case": "layout", + "textregions": { + "paragraph": 1, + "drop-capital": 1, + "header": 2, + "heading": 2, + "marginalia": 3 + }, + "imageregion": 4, + "separatorregion": 5, + "graphicregions": { + "rest_as_decoration": 6 + }, + "artificial_class_on_boundary": ["paragraph", "header", "heading", "marginalia"], + "artificial_class_label": 7 +} +``` + +This implies that the artificial class label, denoted by 7, will be present on PNG files and will only be added to the +elements labeled as "paragraph," "header," "heading," and "marginalia." + +For "textline", "word", and "glyph", the artificial class on the boundaries will be activated only if the +"artificial_class_label" key is specified in the config file. Its value should be set as 2 since these elements +represent binary cases. For example, if the background and textline are denoted as 0 and 1 respectively, then the +artificial class should be assigned the value 2. The example JSON config file should look like this for "textline" use +case: + +```yaml +{ + "use_case": "textline", + "artificial_class_label": 2 +} +``` + +If the coordinates of "PrintSpace" or "Border" are present in the page XML ground truth files, and the user wishes to +crop only the print space area, this can be achieved by activating the "-ps" argument. However, it should be noted that +in this scenario, since cropping will be applied to the label files, the directory of the original images must be +provided to ensure that they are cropped in sync with the labels. This ensures that the correct images and labels +required for training are obtained. The command should resemble the following: + +`python generate_gt_for_training.py pagexml2label -dx "dir of GT xml files" -do "dir where output label png files will +be written" -cfg "custom config json file" -to "output type which has 2d and 3d. 2d is used for training and 3d is just +to visualise the labels" -ps -di "dir where the org images are located" -doi "dir where the cropped output images will +be written" ` + +## Train a model +### classification + +For the classification use case, we haven't provided a ground truth generator, as it's unnecessary. For classification, +all we require is a training directory with subdirectories, each containing images of its respective classes. We need +separate directories for training and evaluation, and the class names (subdirectories) must be consistent across both +directories. Additionally, the class names should be specified in the config JSON file, as shown in the following +example. If, for instance, we aim to classify "apple" and "orange," with a total of 2 classes, the +"classification_classes_name" key in the config file should appear as follows: + +```yaml +{ + "backbone_type" : "nontransformer", + "task": "classification", + "n_classes" : 2, + "n_epochs" : 10, + "input_height" : 448, + "input_width" : 448, + "weight_decay" : 1e-6, + "n_batch" : 4, + "learning_rate": 1e-4, + "f1_threshold_classification": 0.8, + "pretraining" : true, + "classification_classes_name" : {"0":"apple", "1":"orange"}, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +The "dir_train" should be like this: + +``` +. +└── train # train directory + ├── apple # directory of images for apple class + └── orange # directory of images for orange class +``` + +And the "dir_eval" the same structure as train directory: + +``` +. +└── eval # evaluation directory + ├── apple # directory of images for apple class + └── orange # directory of images for orange class + +``` + +The classification model can be trained using the following command line: + +`python train.py with config_classification.json` + +As evident in the example JSON file above, for classification, we utilize a "f1_threshold_classification" parameter. +This parameter is employed to gather all models with an evaluation f1 score surpassing this threshold. Subsequently, +an ensemble of these model weights is executed, and a model is saved in the output directory as "model_ens_avg". +Additionally, the weight of the best model based on the evaluation f1 score is saved as "model_best". + +### reading order +An example config json file for machine based reading order should be like this: + +```yaml +{ + "backbone_type" : "nontransformer", + "task": "reading_order", + "n_classes" : 1, + "n_epochs" : 5, + "input_height" : 672, + "input_width" : 448, + "weight_decay" : 1e-6, + "n_batch" : 4, + "learning_rate": 1e-4, + "pretraining" : true, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +The "dir_train" should be like this: + +``` +. +└── train # train directory + ├── images # directory of images + └── labels # directory of labels +``` + +And the "dir_eval" the same structure as train directory: + +``` +. +└── eval # evaluation directory + ├── images # directory of images + └── labels # directory of labels +``` + +The classification model can be trained like the classification case command line. + +### Segmentation (Textline, Binarization, Page extraction and layout) and enhancement + +#### Parameter configuration for segmentation or enhancement usecases +The following parameter configuration can be applied to all segmentation use cases and enhancements. The augmentation, +its sub-parameters, and continued training are defined only for segmentation use cases and enhancements, not for +classification and machine-based reading order, as you can see in their example config files. + +* backbone_type: For segmentation tasks (such as text line, binarization, and layout detection) and enhancement, we +* offer two backbone options: a "nontransformer" and a "transformer" backbone. For the "transformer" backbone, we first +* apply a CNN followed by a transformer. In contrast, the "nontransformer" backbone utilizes only a CNN ResNet-50. +* task : The task parameter can have values such as "segmentation", "enhancement", "classification", and "reading_order". +* patches: If you want to break input images into smaller patches (input size of the model) you need to set this +* parameter to ``true``. In the case that the model should see the image once, like page extraction, patches should be +* set to ``false``. +* n_batch: Number of batches at each iteration. +* n_classes: Number of classes. In the case of binary classification this should be 2. In the case of reading_order it +* should set to 1. And for the case of layout detection just the unique number of classes should be given. +* n_epochs: Number of epochs. +* input_height: This indicates the height of model's input. +* input_width: This indicates the width of model's input. +* weight_decay: Weight decay of l2 regularization of model layers. +* pretraining: Set to ``true`` to load pretrained weights of ResNet50 encoder. The downloaded weights should be saved +* in a folder named "pretrained_model" in the same directory of "train.py" script. +* augmentation: If you want to apply any kind of augmentation this parameter should first set to ``true``. +* flip_aug: If ``true``, different types of filp will be applied on image. Type of flips is given with "flip_index" parameter. +* blur_aug: If ``true``, different types of blurring will be applied on image. Type of blurrings is given with "blur_k" parameter. +* scaling: If ``true``, scaling will be applied on image. Scale of scaling is given with "scales" parameter. +* degrading: If ``true``, degrading will be applied to the image. The amount of degrading is defined with "degrade_scales" parameter. +* brightening: If ``true``, brightening will be applied to the image. The amount of brightening is defined with "brightness" parameter. +* rotation_not_90: If ``true``, rotation (not 90 degree) will be applied on image. Rotation angles are given with "thetha" parameter. +* rotation: If ``true``, 90 degree rotation will be applied on image. +* binarization: If ``true``,Otsu thresholding will be applied to augment the input data with binarized images. +* scaling_bluring: If ``true``, combination of scaling and blurring will be applied on image. +* scaling_binarization: If ``true``, combination of scaling and binarization will be applied on image. +* scaling_flip: If ``true``, combination of scaling and flip will be applied on image. +* flip_index: Type of flips. +* blur_k: Type of blurrings. +* scales: Scales of scaling. +* brightness: The amount of brightenings. +* thetha: Rotation angles. +* degrade_scales: The amount of degradings. +* continue_training: If ``true``, it means that you have already trained a model and you would like to continue the training. So it is needed to provide the dir of trained model with "dir_of_start_model" and index for naming the models. For example if you have already trained for 3 epochs then your last index is 2 and if you want to continue from model_1.h5, you can set ``index_start`` to 3 to start naming model with index 3. +* weighted_loss: If ``true``, this means that you want to apply weighted categorical_crossentropy as loss fucntion. Be carefull if you set to ``true``the parameter "is_loss_soft_dice" should be ``false`` +* data_is_provided: If you have already provided the input data you can set this to ``true``. Be sure that the train and eval data are in "dir_output". Since when once we provide training data we resize and augment them and then we write them in sub-directories train and eval in "dir_output". +* dir_train: This is the directory of "images" and "labels" (dir_train should include two subdirectories with names of images and labels ) for raw images and labels. Namely they are not prepared (not resized and not augmented) yet for training the model. When we run this tool these raw data will be transformed to suitable size needed for the model and they will be written in "dir_output" in train and eval directories. Each of train and eval include "images" and "labels" sub-directories. +* index_start: Starting index for saved models in the case that "continue_training" is ``true``. +* dir_of_start_model: Directory containing pretrained model to continue training the model in the case that "continue_training" is ``true``. +* transformer_num_patches_xy: Number of patches for vision transformer in x and y direction respectively. +* transformer_patchsize_x: Patch size of vision transformer patches in x direction. +* transformer_patchsize_y: Patch size of vision transformer patches in y direction. +* transformer_projection_dim: Transformer projection dimension. Default value is 64. +* transformer_mlp_head_units: Transformer Multilayer Perceptron (MLP) head units. Default value is [128, 64]. +* transformer_layers: transformer layers. Default value is 8. +* transformer_num_heads: Transformer number of heads. Default value is 4. +* transformer_cnn_first: We have two types of vision transformers. In one type, a CNN is applied first, followed by a transformer. In the other type, this order is reversed. If transformer_cnn_first is true, it means the CNN will be applied before the transformer. Default value is true. + +In the case of segmentation and enhancement the train and evaluation directory should be as following. + +The "dir_train" should be like this: + +``` +. +└── train # train directory + ├── images # directory of images + └── labels # directory of labels +``` + +And the "dir_eval" the same structure as train directory: + +``` +. +└── eval # evaluation directory + ├── images # directory of images + └── labels # directory of labels +``` + +After configuring the JSON file for segmentation or enhancement, training can be initiated by running the following +command, similar to the process for classification and reading order: + +`python train.py with config_classification.json` + +#### Binarization +An example config json file for binarization can be like this: + +```yaml +{ + "backbone_type" : "transformer", + "task": "binarization", + "n_classes" : 2, + "n_epochs" : 4, + "input_height" : 224, + "input_width" : 672, + "weight_decay" : 1e-6, + "n_batch" : 1, + "learning_rate": 1e-4, + "patches" : true, + "pretraining" : true, + "augmentation" : true, + "flip_aug" : false, + "blur_aug" : false, + "scaling" : true, + "degrading": false, + "brightening": false, + "binarization" : false, + "scaling_bluring" : false, + "scaling_binarization" : false, + "scaling_flip" : false, + "rotation": false, + "rotation_not_90": false, + "transformer_num_patches_xy": [7, 7], + "transformer_patchsize_x": 3, + "transformer_patchsize_y": 1, + "transformer_projection_dim": 192, + "transformer_mlp_head_units": [128, 64], + "transformer_layers": 8, + "transformer_num_heads": 4, + "transformer_cnn_first": true, + "blur_k" : ["blur","guass","median"], + "scales" : [0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.4], + "brightness" : [1.3, 1.5, 1.7, 2], + "degrade_scales" : [0.2, 0.4], + "flip_index" : [0, 1, -1], + "thetha" : [10, -10], + "continue_training": false, + "index_start" : 0, + "dir_of_start_model" : " ", + "weighted_loss": false, + "is_loss_soft_dice": false, + "data_is_provided": false, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +#### Textline + +```yaml +{ + "backbone_type" : "nontransformer", + "task": "segmentation", + "n_classes" : 2, + "n_epochs" : 4, + "input_height" : 448, + "input_width" : 224, + "weight_decay" : 1e-6, + "n_batch" : 1, + "learning_rate": 1e-4, + "patches" : true, + "pretraining" : true, + "augmentation" : true, + "flip_aug" : false, + "blur_aug" : false, + "scaling" : true, + "degrading": false, + "brightening": false, + "binarization" : false, + "scaling_bluring" : false, + "scaling_binarization" : false, + "scaling_flip" : false, + "rotation": false, + "rotation_not_90": false, + "blur_k" : ["blur","guass","median"], + "scales" : [0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.4], + "brightness" : [1.3, 1.5, 1.7, 2], + "degrade_scales" : [0.2, 0.4], + "flip_index" : [0, 1, -1], + "thetha" : [10, -10], + "continue_training": false, + "index_start" : 0, + "dir_of_start_model" : " ", + "weighted_loss": false, + "is_loss_soft_dice": false, + "data_is_provided": false, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +#### Enhancement + +```yaml +{ + "backbone_type" : "nontransformer", + "task": "enhancement", + "n_classes" : 3, + "n_epochs" : 4, + "input_height" : 448, + "input_width" : 224, + "weight_decay" : 1e-6, + "n_batch" : 4, + "learning_rate": 1e-4, + "patches" : true, + "pretraining" : true, + "augmentation" : true, + "flip_aug" : false, + "blur_aug" : false, + "scaling" : true, + "degrading": false, + "brightening": false, + "binarization" : false, + "scaling_bluring" : false, + "scaling_binarization" : false, + "scaling_flip" : false, + "rotation": false, + "rotation_not_90": false, + "blur_k" : ["blur","guass","median"], + "scales" : [0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.4], + "brightness" : [1.3, 1.5, 1.7, 2], + "degrade_scales" : [0.2, 0.4], + "flip_index" : [0, 1, -1], + "thetha" : [10, -10], + "continue_training": false, + "index_start" : 0, + "dir_of_start_model" : " ", + "weighted_loss": false, + "is_loss_soft_dice": false, + "data_is_provided": false, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +It's important to mention that the value of n_classes for enhancement should be 3, as the model's output is a 3-channel +image. + +#### Page extraction + +```yaml +{ + "backbone_type" : "nontransformer", + "task": "segmentation", + "n_classes" : 2, + "n_epochs" : 4, + "input_height" : 448, + "input_width" : 224, + "weight_decay" : 1e-6, + "n_batch" : 1, + "learning_rate": 1e-4, + "patches" : false, + "pretraining" : true, + "augmentation" : false, + "flip_aug" : false, + "blur_aug" : false, + "scaling" : true, + "degrading": false, + "brightening": false, + "binarization" : false, + "scaling_bluring" : false, + "scaling_binarization" : false, + "scaling_flip" : false, + "rotation": false, + "rotation_not_90": false, + "blur_k" : ["blur","guass","median"], + "scales" : [0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.4], + "brightness" : [1.3, 1.5, 1.7, 2], + "degrade_scales" : [0.2, 0.4], + "flip_index" : [0, 1, -1], + "thetha" : [10, -10], + "continue_training": false, + "index_start" : 0, + "dir_of_start_model" : " ", + "weighted_loss": false, + "is_loss_soft_dice": false, + "data_is_provided": false, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` + +For page segmentation (or printspace or border segmentation), the model needs to view the input image in its entirety, +hence the patches parameter should be set to false. + +#### layout segmentation +An example config json file for layout segmentation with 5 classes (including background) can be like this: + +```yaml +{ + "backbone_type" : "transformer", + "task": "segmentation", + "n_classes" : 5, + "n_epochs" : 4, + "input_height" : 448, + "input_width" : 224, + "weight_decay" : 1e-6, + "n_batch" : 1, + "learning_rate": 1e-4, + "patches" : true, + "pretraining" : true, + "augmentation" : true, + "flip_aug" : false, + "blur_aug" : false, + "scaling" : true, + "degrading": false, + "brightening": false, + "binarization" : false, + "scaling_bluring" : false, + "scaling_binarization" : false, + "scaling_flip" : false, + "rotation": false, + "rotation_not_90": false, + "transformer_num_patches_xy": [7, 14], + "transformer_patchsize_x": 1, + "transformer_patchsize_y": 1, + "transformer_projection_dim": 64, + "transformer_mlp_head_units": [128, 64], + "transformer_layers": 8, + "transformer_num_heads": 4, + "transformer_cnn_first": true, + "blur_k" : ["blur","guass","median"], + "scales" : [0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.4], + "brightness" : [1.3, 1.5, 1.7, 2], + "degrade_scales" : [0.2, 0.4], + "flip_index" : [0, 1, -1], + "thetha" : [10, -10], + "continue_training": false, + "index_start" : 0, + "dir_of_start_model" : " ", + "weighted_loss": false, + "is_loss_soft_dice": false, + "data_is_provided": false, + "dir_train": "./train", + "dir_eval": "./eval", + "dir_output": "./output" +} +``` +## Inference with the trained model + +### classification +For conducting inference with a trained model, you simply need to execute the following command line, specifying the +directory of the model and the image on which to perform inference: + +`python inference.py -m "model dir" -i "image" ` + +This will straightforwardly return the class of the image. + +### machine based reading order +To infer the reading order using a reading order model, we need a page XML file containing layout information but +without the reading order. We simply need to provide the model directory, the XML file, and the output directory. +The new XML file with the added reading order will be written to the output directory with the same name. +We need to run: + +`python inference.py -m "model dir" -xml "page xml file" -o "output dir to write new xml with reading order" ` + +### Segmentation (Textline, Binarization, Page extraction and layout) and enhancement +For conducting inference with a trained model for segmentation and enhancement you need to run the following command +line: + +`python inference.py -m "model dir" -i "image" -p -s "output image" ` + +Note that in the case of page extraction the -p flag is not needed. + +For segmentation or binarization tasks, if a ground truth (GT) label is available, the IoU evaluation metric can be +calculated for the output. To do this, you need to provide the GT label using the argument -gt. From 3a55b6ce91efda9c170152e8e364e5151c83f14c Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:11:18 +0100 Subject: [PATCH 332/412] consolidate usage documentation --- docs/models.md | 2 ++ docs/usage.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 docs/usage.md diff --git a/docs/models.md b/docs/models.md index c6f7340..ac563b0 100644 --- a/docs/models.md +++ b/docs/models.md @@ -16,8 +16,10 @@ Two Arabic/Persian terms form the name of the model suite: عين الله, whic "eynollah"; it translates into English as "God's Eye" -- it sees (nearly) everything on the document image. See the flowchart below for the different stages and how they interact: + ![](https://user-images.githubusercontent.com/952378/100619946-1936f680-331e-11eb-9297-6e8b4cab3c16.png) + ## Models ### Image enhancement diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..22443a2 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,97 @@ +# Usage documentation +The command-line interface can be called like this: + +```sh +eynollah \ + -i | -di \ + -o \ + -m \ + [OPTIONS] +``` + +The following options can be used to further configure the processing: + +| option | description | +|-------------------|:-------------------------------------------------------------------------------| +| `-fl` | full layout analysis including all steps and segmentation classes | +| `-light` | lighter and faster but simpler method for main region detection and deskewing | +| `-tab` | apply table detection | +| `-ae` | apply enhancement (the resulting image is saved to the output directory) | +| `-as` | apply scaling | +| `-cl` | apply contour detection for curved text lines instead of bounding boxes | +| `-ib` | apply binarization (the resulting image is saved to the output directory) | +| `-ep` | enable plotting (MUST always be used with `-sl`, `-sd`, `-sa`, `-si` or `-ae`) | +| `-eoi` | extract only images to output directory (other processing will not be done) | +| `-ho` | ignore headers for reading order dectection | +| `-si ` | save image regions detected to this directory | +| `-sd ` | save deskewed image to this directory | +| `-sl ` | save layout prediction as plot to this directory | +| `-sp ` | save cropped page image to this directory | +| `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | + +If no option is set, the tool performs layout detection of main regions (background, text, images, separators and marginals). + +The best output quality is produced when RGB images are used as input rather than greyscale or binarized images. + +### `--full-layout` vs `--no-full-layout` + +Here are the difference in elements detected depending on the `--full-layout`/`--no-full-layout` command line flags: + +| | `--full-layout` | `--no-full-layout` | +|--------------------------|-----------------|--------------------| +| reading order | x | x | +| header regions | x | - | +| text regions | x | x | +| text regions / text line | x | x | +| drop-capitals | x | - | +| marginals | x | x | +| marginals / text line | x | x | +| image region | x | x | + +## Use as OCR-D processor +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor that is described in [`ocrd-tool.json`](https://github.com/qurator-spk/eynollah/tree/main/src/eynollah/ocrd-tool.json). + +The source image file group with (preferably) RGB images should be used as input for Eynollah like this: + +``` +ocrd-eynollah-segment -I OCR-D-IMG -O SEG-LINE -P models +``` + +Any image referenced by `@imageFilename` in PAGE-XML is passed on directly to Eynollah as a processor, so that e.g. + +``` +ocrd-eynollah-segment -I OCR-D-IMG-BIN -O SEG-LINE -P models +``` + +uses the original (RGB) image despite any binarization that may have occured in previous OCR-D processing steps. + +## Use with Docker +TODO + +## Hints +* If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, +text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions +separately as much as possible. + +* If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi +(pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will +occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml +data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. + +* For some documents, while the quality is good, their scale is very large, and the performance of tool decreases. In +such cases you can set `-as` (**a**llow **s**caling) to `true`. With this option enabled, the tool will try to rescale +the image and only then the layout detection process will begin. + +* If you care about drop capitals (initials) and headings, you can set `-fl` (**f**ull **l**ayout) to `true`. With this +setting, the tool can currently distinguish 7 document layout classes/elements. + +* In cases where the document includes curved headers or curved lines, rectangular bounding boxes for textlines will not +be a great option. In such cases it is strongly recommended setting the flag `-cl` (**c**urved **l**ines) to `true` to +find contours of curved lines instead of rectangular bounding boxes. Be advised that enabling this option increases the +processing time of the tool. + +* To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide +a directory path to store the extracted images. + +* To extract only images from a document, set the parameter `-eoi` (**e**xtract **o**nly **i**mages). Choosing this +option disables any other processing. To save the cropped images add `-ep` and `-si`. From 0e9a72ea522609e3d45cc9114e1c5b96219d3434 Mon Sep 17 00:00:00 2001 From: cneud <952378+cneud@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:14:59 +0100 Subject: [PATCH 333/412] consolidate usage documentation --- docs/usage.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 22443a2..da164de 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,6 +9,7 @@ eynollah \ [OPTIONS] ``` +## Processing options The following options can be used to further configure the processing: | option | description | @@ -29,9 +30,7 @@ The following options can be used to further configure the processing: | `-sp ` | save cropped page image to this directory | | `-sa ` | save all (plot, enhanced/binary image, layout) to this directory | -If no option is set, the tool performs layout detection of main regions (background, text, images, separators and marginals). - -The best output quality is produced when RGB images are used as input rather than greyscale or binarized images. +If no option is set, the tool performs detection of main regions (background, text, images, separators and marginals). ### `--full-layout` vs `--no-full-layout` @@ -49,7 +48,8 @@ Here are the difference in elements detected depending on the `--full-layout`/`- | image region | x | x | ## Use as OCR-D processor -Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor that is described in [`ocrd-tool.json`](https://github.com/qurator-spk/eynollah/tree/main/src/eynollah/ocrd-tool.json). +Eynollah ships with a CLI interface to be used as [OCR-D](https://ocr-d.de) processor that is described in +[`ocrd-tool.json`](https://github.com/qurator-spk/eynollah/tree/main/src/eynollah/ocrd-tool.json). The source image file group with (preferably) RGB images should be used as input for Eynollah like this: @@ -69,29 +69,24 @@ uses the original (RGB) image despite any binarization that may have occured in TODO ## Hints +* The best output quality is produced when RGB images are used as input rather than greyscale or binarized images. * If none of the parameters is set to `true`, the tool will perform a layout detection of main regions (background, text, images, separators and marginals). An advantage of this tool is that it tries to extract main text regions separately as much as possible. - * If you set `-ae` (**a**llow image **e**nhancement) parameter to `true`, the tool will first check the ppi (pixel-per-inch) of the image and when it is less than 300, the tool will resize it and only then image enhancement will occur. Image enhancement can also take place without this option, but by setting this option to `true`, the layout xml data (e.g. coordinates) will be based on the resized and enhanced image instead of the original image. - * For some documents, while the quality is good, their scale is very large, and the performance of tool decreases. In such cases you can set `-as` (**a**llow **s**caling) to `true`. With this option enabled, the tool will try to rescale the image and only then the layout detection process will begin. - * If you care about drop capitals (initials) and headings, you can set `-fl` (**f**ull **l**ayout) to `true`. With this setting, the tool can currently distinguish 7 document layout classes/elements. - * In cases where the document includes curved headers or curved lines, rectangular bounding boxes for textlines will not be a great option. In such cases it is strongly recommended setting the flag `-cl` (**c**urved **l**ines) to `true` to find contours of curved lines instead of rectangular bounding boxes. Be advised that enabling this option increases the processing time of the tool. - * To crop and save image regions inside the document, set the parameter `-si` (**s**ave **i**mages) to true and provide a directory path to store the extracted images. - * To extract only images from a document, set the parameter `-eoi` (**e**xtract **o**nly **i**mages). Choosing this option disables any other processing. To save the cropped images add `-ep` and `-si`. From c9de578d4de50e77aff20f2aba79e6cce800381e Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 28 Mar 2025 11:25:03 +0100 Subject: [PATCH 334/412] removing imutils from requirements --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ef3fe31..9b821c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 -imutils >= 0.5.3 numba <= 0.58.1 loky From f756b08c9ba0ae07f3ba756f46447b6e417bf354 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 28 Mar 2025 14:57:40 +0100 Subject: [PATCH 335/412] Revert "replace usages of `imutils` with opencv equivalents" --- src/eynollah/utils/__init__.py | 1 + src/eynollah/utils/rotate.py | 74 +++++++++------------------------- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index faa32b0..a67fc38 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -4,6 +4,7 @@ import matplotlib.pyplot as plt import numpy as np from shapely import geometry import cv2 +import imutils from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d import time diff --git a/src/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py index 734f924..603c2d9 100644 --- a/src/eynollah/utils/rotate.py +++ b/src/eynollah/utils/rotate.py @@ -1,5 +1,6 @@ import math -import numpy as np + +import imutils import cv2 def rotatedRectWithMaxArea(w, h, angle): @@ -10,11 +11,11 @@ def rotatedRectWithMaxArea(w, h, angle): side_long, side_short = (w, h) if width_is_longer else (h, w) # since the solutions for angle, -angle and 180-angle are all the same, - # it suffices to look at the first quadrant and the absolute values of sin,cos: + # if suffices to look at the first quadrant and the absolute values of sin,cos: sin_a, cos_a = abs(math.sin(angle)), abs(math.cos(angle)) if side_short <= 2.0 * sin_a * cos_a * side_long or abs(sin_a - cos_a) < 1e-10: - # half constrained case: two crop corners touch the longer side, - # the other two corners are on the mid-line parallel to the longer line + # half constrained case: two crop corners touch the longer side, + # the other two corners are on the mid-line parallel to the longer line x = 0.5 * side_short wr, hr = (x / sin_a, x / cos_a) if width_is_longer else (x / cos_a, x / sin_a) else: @@ -24,45 +25,6 @@ def rotatedRectWithMaxArea(w, h, angle): return wr, hr - -def rotate_image_opencv(image, angle): - # Calculate the original image dimensions (h, w) and the center point (cx, cy) - h, w = image.shape[:2] - cx, cy = (w // 2, h // 2) - - # Compute the rotation matrix - M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0) - - # Calculate the new bounding box - corners = np.array([ - [0, 0], - [w, 0], - [w, h], - [0, h] - ]) - - # Apply rotation matrix to the corner points - ones = np.ones(shape=(len(corners), 1)) - corners_ones = np.hstack([corners, ones]) - transformed_corners = M @ corners_ones.T - transformed_corners = transformed_corners.T - - # Calculate the new bounding box dimensions - min_x, min_y = np.min(transformed_corners, axis=0) - max_x, max_y = np.max(transformed_corners, axis=0) - - newW = int(np.ceil(max_x - min_x)) - newH = int(np.ceil(max_y - min_y)) - - # Adjust the rotation matrix to account for translation - M[0, 2] += (newW / 2) - cx - M[1, 2] += (newH / 2) - cy - - # Perform the affine transformation (rotation) - rotated_image = cv2.warpAffine(image, M, (newW, newH)) - - return rotated_image - def rotate_max_area_new(image, rotated, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) h, w, _ = rotated.shape @@ -73,7 +35,7 @@ def rotate_max_area_new(image, rotated, angle): return rotated[y1:y2, x1:x2] def rotation_image_new(img, thetha): - rotated = rotate_image_opencv(img, thetha) + rotated = imutils.rotate(img, thetha) return rotate_max_area_new(img, rotated, thetha) def rotate_image(img_patch, slope): @@ -82,10 +44,13 @@ def rotate_image(img_patch, slope): M = cv2.getRotationMatrix2D(center, slope, 1.0) return cv2.warpAffine(img_patch, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) -def rotate_image_different(img, slope): +def rotate_image_different( img, slope): + # img = cv2.imread('images/input.jpg') num_rows, num_cols = img.shape[:2] + rotation_matrix = cv2.getRotationMatrix2D((num_cols / 2, num_rows / 2), slope, 1) - return cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows)) + img_rotation = cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows)) + return img_rotation def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_table_prediction, angle): wr, hr = rotatedRectWithMaxArea(image.shape[1], image.shape[0], math.radians(angle)) @@ -97,17 +62,17 @@ def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_ta return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_table_prediction[y1:y2, x1:x2] def rotation_not_90_func(img, textline, text_regions_p_1, table_prediction, thetha): - rotated = rotate_image_opencv(img, thetha) - rotated_textline = rotate_image_opencv(textline, thetha) - rotated_layout = rotate_image_opencv(text_regions_p_1, thetha) - rotated_table_prediction = rotate_image_opencv(table_prediction, thetha) + rotated = imutils.rotate(img, thetha) + rotated_textline = imutils.rotate(textline, thetha) + rotated_layout = imutils.rotate(text_regions_p_1, thetha) + rotated_table_prediction = imutils.rotate(table_prediction, thetha) return rotate_max_area(img, rotated, rotated_textline, rotated_layout, rotated_table_prediction, thetha) def rotation_not_90_func_full_layout(img, textline, text_regions_p_1, text_regions_p_fully, thetha): - rotated = rotate_image_opencv(img, thetha) - rotated_textline = rotate_image_opencv(textline, thetha) - rotated_layout = rotate_image_opencv(text_regions_p_1, thetha) - rotated_layout_full = rotate_image_opencv(text_regions_p_fully, thetha) + rotated = imutils.rotate(img, thetha) + rotated_textline = imutils.rotate(textline, thetha) + rotated_layout = imutils.rotate(text_regions_p_1, thetha) + rotated_layout_full = imutils.rotate(text_regions_p_fully, thetha) return rotate_max_area_full_layout(img, rotated, rotated_textline, rotated_layout, rotated_layout_full, thetha) def rotate_max_area_full_layout(image, rotated, rotated_textline, rotated_layout, rotated_layout_full, angle): @@ -118,3 +83,4 @@ def rotate_max_area_full_layout(image, rotated, rotated_textline, rotated_layout x1 = w // 2 - int(wr / 2) x2 = x1 + int(wr) return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_layout_full[y1:y2, x1:x2] + From b55389ac62f2b32455ee9d15b849777381ee740b Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 28 Mar 2025 14:59:31 +0100 Subject: [PATCH 336/412] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9b821c3..ef3fe31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 +imutils >= 0.5.3 numba <= 0.58.1 loky From cf40f9ecc5afb4fec2bc9b815ad3250fbde42728 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Fri, 28 Mar 2025 20:58:32 +0100 Subject: [PATCH 337/412] The rotate_image function produces the exact same rotation as Imutils. Therefore, there is no need to retain the remove-imutils-1 branch. --- requirements.txt | 1 - src/eynollah/utils/__init__.py | 1 - src/eynollah/utils/rotate.py | 20 +++++++++----------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index ef3fe31..9b821c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ ocrd >= 2.23.3 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 -imutils >= 0.5.3 numba <= 0.58.1 loky diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index a67fc38..faa32b0 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -4,7 +4,6 @@ import matplotlib.pyplot as plt import numpy as np from shapely import geometry import cv2 -import imutils from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d import time diff --git a/src/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py index 603c2d9..0f2c177 100644 --- a/src/eynollah/utils/rotate.py +++ b/src/eynollah/utils/rotate.py @@ -1,6 +1,4 @@ import math - -import imutils import cv2 def rotatedRectWithMaxArea(w, h, angle): @@ -35,7 +33,7 @@ def rotate_max_area_new(image, rotated, angle): return rotated[y1:y2, x1:x2] def rotation_image_new(img, thetha): - rotated = imutils.rotate(img, thetha) + rotated = rotate_image(img, thetha) return rotate_max_area_new(img, rotated, thetha) def rotate_image(img_patch, slope): @@ -62,17 +60,17 @@ def rotate_max_area(image, rotated, rotated_textline, rotated_layout, rotated_ta return rotated[y1:y2, x1:x2], rotated_textline[y1:y2, x1:x2], rotated_layout[y1:y2, x1:x2], rotated_table_prediction[y1:y2, x1:x2] def rotation_not_90_func(img, textline, text_regions_p_1, table_prediction, thetha): - rotated = imutils.rotate(img, thetha) - rotated_textline = imutils.rotate(textline, thetha) - rotated_layout = imutils.rotate(text_regions_p_1, thetha) - rotated_table_prediction = imutils.rotate(table_prediction, thetha) + rotated = rotate_image(img, thetha) + rotated_textline = rotate_image(textline, thetha) + rotated_layout = rotate_image(text_regions_p_1, thetha) + rotated_table_prediction = rotate_image(table_prediction, thetha) return rotate_max_area(img, rotated, rotated_textline, rotated_layout, rotated_table_prediction, thetha) def rotation_not_90_func_full_layout(img, textline, text_regions_p_1, text_regions_p_fully, thetha): - rotated = imutils.rotate(img, thetha) - rotated_textline = imutils.rotate(textline, thetha) - rotated_layout = imutils.rotate(text_regions_p_1, thetha) - rotated_layout_full = imutils.rotate(text_regions_p_fully, thetha) + rotated = rotate_image(img, thetha) + rotated_textline = rotate_image(textline, thetha) + rotated_layout = rotate_image(text_regions_p_1, thetha) + rotated_layout_full = rotate_image(text_regions_p_fully, thetha) return rotate_max_area_full_layout(img, rotated, rotated_textline, rotated_layout, rotated_layout_full, thetha) def rotate_max_area_full_layout(image, rotated, rotated_textline, rotated_layout, rotated_layout_full, angle): From 9b04688ebcee52fd913af9d70adb8471b9f3bee8 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Sun, 30 Mar 2025 15:34:27 +0200 Subject: [PATCH 338/412] The rotate_image function has been updated. Additionally, the reading order is now correct in the case of the light version, provided that slope_deskew exceeds the slope_threshold. --- src/eynollah/eynollah.py | 27 ++++++++++++++++++--------- src/eynollah/utils/rotate.py | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 34fc8cb..9ead53e 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4030,7 +4030,7 @@ class Eynollah: all_found_textline_polygons[j][ij][:,0,0] = con_scaled[:,0, 0] return all_found_textline_polygons - def filter_contours_inside_a_bigger_one(self,contours, image, marginal_cnts=None, type_contour="textregion"): + def filter_contours_inside_a_bigger_one(self,contours, contours_d_ordered, image, marginal_cnts=None, type_contour="textregion"): if type_contour=="textregion": areas = [cv2.contourArea(contours[j]) for j in range(len(contours))] area_tot = image.shape[0]*image.shape[1] @@ -4067,8 +4067,10 @@ class Eynollah: indexes_to_be_removed = np.sort(indexes_to_be_removed)[::-1] for ind in indexes_to_be_removed: contours.pop(ind) + if len(contours_d_ordered)>0: + contours_d_ordered.pop(ind) - return contours + return contours, contours_d_ordered else: contours_txtline_of_all_textregions = [] @@ -4375,7 +4377,7 @@ class Eynollah: all_found_textline_polygons = self.dilate_textregions_contours_textline_version( all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one( - all_found_textline_polygons, textline_mask_tot_ea, type_contour="textline") + all_found_textline_polygons, None, textline_mask_tot_ea, type_contour="textline") order_text_new = [0] @@ -4417,9 +4419,9 @@ class Eynollah: textline_mask_tot_ea_deskew = resize_image(textline_mask_tot_ea,img_h_new, img_w_new ) - slope_deskew, slope_first = 0, 0 #self.run_deskew(textline_mask_tot_ea_deskew) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea_deskew) else: - slope_deskew, slope_first = 0, 0 #self.run_deskew(textline_mask_tot_ea) + slope_deskew, slope_first = self.run_deskew(textline_mask_tot_ea) #print("text region early -2,5 in %.1fs", time.time() - t0) #self.logger.info("Textregion detection took %.1fs ", time.time() - t1t) num_col, num_col_classifier, img_only_regions, page_coord, image_page, mask_images, mask_lines, \ @@ -4550,7 +4552,8 @@ class Eynollah: cx_bigest_big, cy_biggest_big, _, _, _, _, _ = find_new_features_of_contours([contours_biggest]) cx_bigest, cy_biggest, _, _, _, _, _ = find_new_features_of_contours(contours_only_text_parent) - + + if np.abs(slope_deskew) >= SLOPE_THRESHOLD: contours_only_text_d, hir_on_text_d = return_contours_of_image(text_only_d) contours_only_text_parent_d = return_parent_contours(contours_only_text_d, hir_on_text_d) @@ -4647,13 +4650,19 @@ class Eynollah: continue else: return pcgts + + + ## check the ro order + + + #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: contours_only_text_parent = self.dilate_textregions_contours( contours_only_text_parent) - contours_only_text_parent = self.filter_contours_inside_a_bigger_one( - contours_only_text_parent, text_only, marginal_cnts=polygons_of_marginals) + contours_only_text_parent , contours_only_text_parent_d_ordered = self.filter_contours_inside_a_bigger_one( + contours_only_text_parent, contours_only_text_parent_d_ordered, text_only, marginal_cnts=polygons_of_marginals) #print("text region early 3.5 in %.1fs", time.time() - t0) txt_con_org = get_textregion_contours_in_org_image_light( contours_only_text_parent, self.image, slope_first, map=self.executor.map) @@ -4690,7 +4699,7 @@ class Eynollah: all_found_textline_polygons = self.dilate_textregions_contours_textline_version( all_found_textline_polygons) all_found_textline_polygons = self.filter_contours_inside_a_bigger_one( - all_found_textline_polygons, textline_mask_tot_ea_org, type_contour="textline") + all_found_textline_polygons, None, textline_mask_tot_ea_org, type_contour="textline") all_found_textline_polygons_marginals = self.dilate_textregions_contours_textline_version( all_found_textline_polygons_marginals) contours_only_text_parent, txt_con_org, all_found_textline_polygons, contours_only_text_parent_d_ordered, \ diff --git a/src/eynollah/utils/rotate.py b/src/eynollah/utils/rotate.py index 0f2c177..189693d 100644 --- a/src/eynollah/utils/rotate.py +++ b/src/eynollah/utils/rotate.py @@ -40,7 +40,7 @@ def rotate_image(img_patch, slope): (h, w) = img_patch.shape[:2] center = (w // 2, h // 2) M = cv2.getRotationMatrix2D(center, slope, 1.0) - return cv2.warpAffine(img_patch, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) + return cv2.warpAffine(img_patch, M, (w, h) ) def rotate_image_different( img, slope): # img = cv2.imread('images/input.jpg') From edf924c2cb93376c465eb025202188f32474acb0 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Sun, 30 Mar 2025 19:47:25 +0200 Subject: [PATCH 339/412] ocrd-tool: add dockerhub --- src/eynollah/ocrd-tool.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json index 3295049..b09c2fa 100644 --- a/src/eynollah/ocrd-tool.json +++ b/src/eynollah/ocrd-tool.json @@ -1,6 +1,7 @@ { "version": "0.3.1", "git_url": "https://github.com/qurator-spk/eynollah", + "dockerhub": "ocrd/eynollah", "tools": { "ocrd-eynollah-segment": { "executable": "ocrd-eynollah-segment", From ea136e3ddda336a988107c0a01d87c2a0b262d97 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 00:30:06 +0200 Subject: [PATCH 340/412] 'overwrite' check: only in 'dir_in' mode --- src/eynollah/eynollah.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 8752272..d065883 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4331,14 +4331,13 @@ class Eynollah: if self.dir_in: self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) #print("text region early -11 in %.1fs", time.time() - t0) + if os.path.exists(self.writer.output_filename): + if self.overwrite: + self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename) + else: + self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) + continue - if os.path.exists(self.writer.output_filename): - if self.overwrite: - self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename) - else: - self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) - continue - img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) if self.extract_only_images: From af4e2a4ffc34df38876a58980da3b66906053ea6 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 00:31:09 +0200 Subject: [PATCH 341/412] do not require 'dir_out' outside 'dir_in' mode --- src/eynollah/writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eynollah/writer.py b/src/eynollah/writer.py index 66747b1..7bcd9af 100644 --- a/src/eynollah/writer.py +++ b/src/eynollah/writer.py @@ -28,7 +28,7 @@ class EynollahXmlWriter(): self.counter = EynollahIdCounter() self.dir_out = dir_out self.image_filename = image_filename - self.output_filename = os.path.join(self.dir_out, self.image_filename_stem) + ".xml" + self.output_filename = os.path.join(self.dir_out or "", self.image_filename_stem) + ".xml" self.curved_line = curved_line self.textline_light = textline_light self.pcgts = pcgts From 238132e26011532bbd92e0739727cb8501f66c53 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 00:31:49 +0200 Subject: [PATCH 342/412] use 'image_filename' for pseudo-iteration outside 'dir_in' mode --- src/eynollah/eynollah.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index d065883..c10b22e 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4323,8 +4323,8 @@ class Eynollah: t0_tot = time.time() if not self.dir_in: - self.ls_imgs = [1] - + self.ls_imgs = [self.image_filename] + for img_name in self.ls_imgs: self.logger.info(img_name) t0 = time.time() From efd3fa6775dd943cb101b90057abe1a809cec2a5 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 00:32:26 +0200 Subject: [PATCH 343/412] allow empty imports for optional dependencies --- src/eynollah/eynollah.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index c10b22e..68a9580 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -24,11 +24,12 @@ from ocrd import OcrdPage from ocrd_utils import getLogger import cv2 import numpy as np -from transformers import TrOCRProcessor -from PIL import Image import torch from difflib import SequenceMatcher as sq -from transformers import VisionEncoderDecoderModel +try: + from transformers import TrOCRProcessor, VisionEncoderDecoderModel +except ImportError: + TrOCRProcessor = VisionEncoderDecoderModel = None from numba import cuda import copy from scipy.signal import find_peaks From 6d02e90570aec880a8fcf978f7888732409c7c65 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:14:54 +0200 Subject: [PATCH 344/412] OCR-D: restrict max_workers=1 --- src/eynollah/processor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index 0dfd7e7..3380b24 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -5,6 +5,9 @@ from ocrd import Processor, OcrdPageResult from .eynollah import Eynollah class EynollahProcessor(Processor): + # already employs background CPU multiprocessing per page + # already employs GPU (without singleton process atm) + max_workers = 1 def setup(self) -> None: # for caching models From 3916474b8b2e9cfd6794832ce486b15a4e67b4d9 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:15:12 +0200 Subject: [PATCH 345/412] OCR-D: require >=v3.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94fbbdc..280eadd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 3.0.0b4 +ocrd >= 3.1.0 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 From 55969b0173a8013fec3c5223c24ebcbaad49fa13 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:15:26 +0200 Subject: [PATCH 346/412] OCR-D: add docstring --- src/eynollah/processor.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index 3380b24..f4db854 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -16,6 +16,27 @@ class EynollahProcessor(Processor): raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection but parameter 'light_mode' is not enabled") def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: + """ + Performs cropping, region and line segmentation with Eynollah. + + For each page, open and deserialize PAGE input file (from existing + PAGE file in the input fileGrp, or generated from image file). + Retrieve its respective page-level image (ignoring annotation that + already added `binarized`, `cropped` or `deskewed` features). + + Set up Eynollah to detect regions and lines, and add each one to the + page, respectively. + + \b + - If ``tables``, try to detect table blocks and add them as TableRegion. + - If ``full_layout``, then in addition to paragraphs and marginals, also + try to detect drop capitals and headings. + - If ``ignore_page_extraction``, then attempt no cropping of the page. + - If ``curved_line``, then compute contour polygons for text lines + instead of simple bounding boxes. + + Produce a new output file by serialising the resulting hierarchy. + """ assert input_pcgts assert input_pcgts[0] assert self.parameter From 4338259ca1be4fbca6c7ffe1a921939c257c8e68 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:17:14 +0200 Subject: [PATCH 347/412] OCR-D: ensure page image gets replaced in result as well if not the original file --- src/eynollah/processor.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index f4db854..812ba25 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -41,18 +41,20 @@ class EynollahProcessor(Processor): assert input_pcgts[0] assert self.parameter pcgts = input_pcgts[0] + result = OcrdPageResult(pcgts) page = pcgts.get_Page() - # if not('://' in page.imageFilename): - # image_filename = next(self.workspace.mets.find_files(local_filename=page.imageFilename)).local_filename - # else: - # # could be a URL with file:// or truly remote - # image_filename = self.workspace.download_file(next(self.workspace.mets.find_files(url=page.imageFilename))).local_filename page_image, _, _ = self.workspace.image_from_page( page, page_id, # avoid any features that would change the coordinate system: cropped,deskewed # (the PAGE builder merely adds regions, so afterwards we would not know which to transform) # also avoid binarization as models usually fare better on grayscale/RGB feature_filter='cropped,deskewed,binarized') + if hasattr(page_image, 'filename'): + image_filename = page_image.filename + else: + image_filename = "dummy" # will be replaced by ocrd.Processor.process_page_file + result.images.append(OcrdPageResultImage(page_image, '.IMG', page)) # mark as new original + # FIXME: mask out already existing regions (incremental segmentation) eynollah = Eynollah( self.resolve_resource(self.parameter['models']), logger=self.logger, @@ -68,7 +70,7 @@ class EynollahProcessor(Processor): tables=self.parameter['tables'], override_dpi=self.parameter['dpi'], pcgts=pcgts, - image_filename=page.imageFilename, + image_filename=image_filename, image_pil=page_image ) if self.models is not None: @@ -76,4 +78,4 @@ class EynollahProcessor(Processor): eynollah.models = self.models eynollah.run() self.models = eynollah.models - return OcrdPageResult(pcgts) + return result From c794d4d29f82eff66d27d4957dc633b13668b3ec Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:46:29 +0200 Subject: [PATCH 348/412] =?UTF-8?q?OCR-D:=20fix=20typo=20light=5Fmode?= =?UTF-8?q?=E2=86=92light=5Fversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/eynollah/ocrd-tool-binarization.json | 47 ------------------------ src/eynollah/ocrd-tool.json | 4 +- src/eynollah/processor.py | 9 ++++- 3 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 src/eynollah/ocrd-tool-binarization.json diff --git a/src/eynollah/ocrd-tool-binarization.json b/src/eynollah/ocrd-tool-binarization.json deleted file mode 100644 index 1711e89..0000000 --- a/src/eynollah/ocrd-tool-binarization.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "version": "0.1.0", - "git_url": "https://github.com/qurator-spk/sbb_binarization", - "tools": { - "ocrd-sbb-binarize": { - "executable": "ocrd-sbb-binarize", - "description": "Pixelwise binarization with selectional auto-encoders in Keras", - "categories": ["Image preprocessing"], - "steps": ["preprocessing/optimization/binarization"], - "input_file_grp": [], - "output_file_grp": [], - "parameters": { - "operation_level": { - "type": "string", - "enum": ["page", "region"], - "default": "page", - "description": "PAGE XML hierarchy level to operate on" - }, - "model": { - "description": "Directory containing HDF5 or SavedModel/ProtoBuf models. Can be an absolute path or a path relative to the OCR-D resource location, the current working directory or the $SBB_BINARIZE_DATA environment variable (if set)", - "type": "string", - "format": "uri", - "content-type": "text/directory", - "required": true - } - }, - "resources": [ - { - "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2020_01_16.zip", - "name": "default", - "type": "archive", - "path_in_archive": "saved_model_2020_01_16", - "size": 563147331, - "description": "default models provided by github.com/qurator-spk (SavedModel format)" - }, - { - "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2021_03_09.zip", - "name": "default-2021-03-09", - "type": "archive", - "path_in_archive": ".", - "size": 133230419, - "description": "updated default models provided by github.com/qurator-spk (SavedModel format)" - } - ] - } - } -} diff --git a/src/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json index e35f874..e7f1585 100644 --- a/src/eynollah/ocrd-tool.json +++ b/src/eynollah/ocrd-tool.json @@ -13,10 +13,10 @@ "parameters": { "models": { "type": "string", - "format": "file", + "format": "uri", "content-type": "text/directory", "cacheable": true, - "description": "Path to directory containing models to be used (See https://qurator-data.de/eynollah)", + "description": "Directory containing models to be used (See https://qurator-data.de/eynollah)", "required": true }, "dpi": { diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index 812ba25..b5b9cb2 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -9,11 +9,16 @@ class EynollahProcessor(Processor): # already employs GPU (without singleton process atm) max_workers = 1 + @property + def executable(self): + return 'ocrd-eynollah-segment' + def setup(self) -> None: # for caching models self.models = None - if self.parameter['textline_light'] and not self.parameter['light_mode']: - raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection but parameter 'light_mode' is not enabled") + if self.parameter['textline_light'] and not self.parameter['light_version']: + raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection, " + "but parameter 'light_version' is not enabled") def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: """ From a1068ff2eb8d941501310bb5c6d9321cbc949486 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 01:47:32 +0200 Subject: [PATCH 349/412] OCR-D: move sbb-binarize to ocrd-tool.json, update to v3 --- pyproject.toml | 2 +- src/eynollah/ocrd-tool.json | 41 +++++++ src/eynollah/ocrd_cli_binarization.py | 151 +++++++++----------------- 3 files changed, 93 insertions(+), 101 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61d488a..66ba733 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ plotting = ["matplotlib"] [project.scripts] eynollah = "eynollah.cli:main" ocrd-eynollah-segment = "eynollah.ocrd_cli:main" -ocrd-sbb-binarize = "eynollah.ocrd_cli_binarization:cli" +ocrd-sbb-binarize = "eynollah.ocrd_cli_binarization:main" [project.urls] Homepage = "https://github.com/qurator-spk/eynollah" diff --git a/src/eynollah/ocrd-tool.json b/src/eynollah/ocrd-tool.json index e7f1585..125131c 100644 --- a/src/eynollah/ocrd-tool.json +++ b/src/eynollah/ocrd-tool.json @@ -91,6 +91,47 @@ "path_in_archive": "models_eynollah" } ] + }, + "ocrd-sbb-binarize": { + "executable": "ocrd-sbb-binarize", + "description": "Pixelwise binarization with selectional auto-encoders in Keras", + "categories": ["Image preprocessing"], + "steps": ["preprocessing/optimization/binarization"], + "input_file_grp_cardinality": 1, + "output_file_grp_cardinality": 1, + "parameters": { + "operation_level": { + "type": "string", + "enum": ["page", "region"], + "default": "page", + "description": "PAGE XML hierarchy level to operate on" + }, + "model": { + "description": "Directory containing HDF5 or SavedModel/ProtoBuf models. Can be an absolute path or a path relative to the OCR-D resource location, the current working directory or the $SBB_BINARIZE_DATA environment variable (if set)", + "type": "string", + "format": "uri", + "content-type": "text/directory", + "required": true + } + }, + "resources": [ + { + "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2020_01_16.zip", + "name": "default", + "type": "archive", + "path_in_archive": "saved_model_2020_01_16", + "size": 563147331, + "description": "default models provided by github.com/qurator-spk (SavedModel format)" + }, + { + "url": "https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2021_03_09.zip", + "name": "default-2021-03-09", + "type": "archive", + "path_in_archive": ".", + "size": 133230419, + "description": "updated default models provided by github.com/qurator-spk (SavedModel format)" + } + ] } } } diff --git a/src/eynollah/ocrd_cli_binarization.py b/src/eynollah/ocrd_cli_binarization.py index 6a8bbdc..6bc4b74 100644 --- a/src/eynollah/ocrd_cli_binarization.py +++ b/src/eynollah/ocrd_cli_binarization.py @@ -1,29 +1,16 @@ -from os import environ -from os.path import join -from pathlib import Path -from pkg_resources import resource_string -from json import loads +from typing import Optional from PIL import Image import numpy as np import cv2 from click import command -from ocrd_utils import ( - getLogger, - assert_file_grp_cardinality, - make_file_id, - MIMETYPE_PAGE -) -from ocrd import Processor -from ocrd_modelfactory import page_from_file -from ocrd_models.ocrd_page import AlternativeImageType, to_xml +from ocrd import Processor, OcrdPageResult, OcrdPageResultImage +from ocrd_models.ocrd_page import OcrdPage, AlternativeImageType from ocrd.decorators import ocrd_cli_options, ocrd_cli_wrap_processor from .sbb_binarize import SbbBinarizer -OCRD_TOOL = loads(resource_string(__name__, 'ocrd-tool-binarization.json').decode('utf8')) -TOOL = 'ocrd-sbb-binarize' def cv2pil(img): return Image.fromarray(img.astype('uint8')) @@ -35,39 +22,22 @@ def pil2cv(img): return cv2.cvtColor(pil_as_np_array, color_conversion) class SbbBinarizeProcessor(Processor): + # already employs GPU (without singleton process atm) + max_workers = 1 - def __init__(self, *args, **kwargs): - kwargs['ocrd_tool'] = OCRD_TOOL['tools'][TOOL] - kwargs['version'] = OCRD_TOOL['version'] - super().__init__(*args, **kwargs) - if hasattr(self, 'output_file_grp'): - # processing context - self.setup() + @property + def executable(self): + return 'ocrd-sbb-binarize' def setup(self): """ Set up the model prior to processing. """ - LOG = getLogger('processor.SbbBinarize.__init__') - if not 'model' in self.parameter: - raise ValueError("'model' parameter is required") - # resolve relative path via environment variable - model_path = Path(self.parameter['model']) - if not model_path.is_absolute(): - if 'SBB_BINARIZE_DATA' in environ and environ['SBB_BINARIZE_DATA']: - LOG.info("Environment variable SBB_BINARIZE_DATA is set to '%s'" \ - " - prepending to model value '%s'. If you don't want this mechanism," \ - " unset the SBB_BINARIZE_DATA environment variable.", - environ['SBB_BINARIZE_DATA'], model_path) - model_path = Path(environ['SBB_BINARIZE_DATA']).joinpath(model_path) - model_path = model_path.resolve() - if not model_path.is_dir(): - raise FileNotFoundError("Does not exist or is not a directory: %s" % model_path) # resolve relative path via OCR-D ResourceManager - model_path = self.resolve_resource(str(model_path)) - self.binarizer = SbbBinarizer(model_dir=model_path, logger=LOG) + model_path = self.resolve_resource(self.parameter['model']) + self.binarizer = SbbBinarizer(model_dir=model_path, logger=self.logger) - def process(self): + def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: """ Binarize images with sbb_binarization (based on selectional auto-encoders). @@ -88,71 +58,52 @@ class SbbBinarizeProcessor(Processor): Produce a new PAGE output file by serialising the resulting hierarchy. """ - LOG = getLogger('processor.SbbBinarize') - assert_file_grp_cardinality(self.input_file_grp, 1) - assert_file_grp_cardinality(self.output_file_grp, 1) - + assert input_pcgts + assert input_pcgts[0] + assert self.parameter oplevel = self.parameter['operation_level'] + pcgts = input_pcgts[0] + result = OcrdPageResult(pcgts) + page = pcgts.get_Page() + page_image, page_xywh, _ = self.workspace.image_from_page( + page, page_id, feature_filter='binarized') - for n, input_file in enumerate(self.input_files): - file_id = make_file_id(input_file, self.output_file_grp) - page_id = input_file.pageId or input_file.ID - LOG.info("INPUT FILE %i / %s", n, page_id) - pcgts = page_from_file(self.workspace.download_file(input_file)) - self.add_metadata(pcgts) - pcgts.set_pcGtsId(file_id) - page = pcgts.get_Page() - page_image, page_xywh, _ = self.workspace.image_from_page(page, page_id, feature_filter='binarized') + if oplevel == 'page': + self.logger.info("Binarizing on 'page' level in page '%s'", page_id) + page_image_bin = cv2pil(self.binarizer.run(image=pil2cv(page_image), use_patches=True)) + # update PAGE (reference the image file): + page_image_ref = AlternativeImageType(comments=page_xywh['features'] + ',binarized,clipped') + page.add_AlternativeImage(page_image_ref) + result.images.append(OcrdPageResultImage(page_image_bin, '.IMG-BIN', page_image_ref)) - if oplevel == 'page': - LOG.info("Binarizing on 'page' level in page '%s'", page_id) - bin_image = cv2pil(self.binarizer.run(image=pil2cv(page_image), use_patches=True)) - # update METS (add the image file): - bin_image_path = self.workspace.save_image_file(bin_image, - file_id + '.IMG-BIN', - page_id=input_file.pageId, - file_grp=self.output_file_grp) - page.add_AlternativeImage(AlternativeImageType(filename=bin_image_path, comments='%s,binarized' % page_xywh['features'])) + elif oplevel == 'region': + regions = page.get_AllRegions(['Text', 'Table'], depth=1) + if not regions: + self.logger.warning("Page '%s' contains no text/table regions", page_id) + for region in regions: + region_image, region_xywh = self.workspace.image_from_segment( + region, page_image, page_xywh, feature_filter='binarized') + region_image_bin = cv2pil(binarizer.run(image=pil2cv(region_image), use_patches=True)) + # update PAGE (reference the image file): + region_image_ref = AlternativeImageType(comments=region_xywh['features'] + ',binarized') + region.add_AlternativeImage(region_image_ref) + result.images.append(OcrdPageResultImage(region_image_bin, region.id + '.IMG-BIN', region_image_ref)) - elif oplevel == 'region': - regions = page.get_AllRegions(['Text', 'Table'], depth=1) - if not regions: - LOG.warning("Page '%s' contains no text/table regions", page_id) - for region in regions: - region_image, region_xywh = self.workspace.image_from_segment(region, page_image, page_xywh, feature_filter='binarized') - region_image_bin = cv2pil(binarizer.run(image=pil2cv(region_image), use_patches=True)) - region_image_bin_path = self.workspace.save_image_file( - region_image_bin, - "%s_%s.IMG-BIN" % (file_id, region.id), - page_id=input_file.pageId, - file_grp=self.output_file_grp) - region.add_AlternativeImage( - AlternativeImageType(filename=region_image_bin_path, comments='%s,binarized' % region_xywh['features'])) + elif oplevel == 'line': + lines = page.get_AllTextLines() + if not lines: + self.logger.warning("Page '%s' contains no text lines", page_id) + for line in lines: + line_image, line_xywh = self.workspace.image_from_segment(line, page_image, page_xywh, feature_filter='binarized') + line_image_bin = cv2pil(binarizer.run(image=pil2cv(line_image), use_patches=True)) + # update PAGE (reference the image file): + line_image_ref = AlternativeImageType(comments=line_xywh['features'] + ',binarized') + line.add_AlternativeImage(region_image_ref) + result.images.append(OcrdPageResultImage(line_image_bin, line.id + '.IMG-BIN', line_image_ref)) - elif oplevel == 'line': - region_line_tuples = [(r.id, r.get_TextLine()) for r in page.get_AllRegions(['Text'], depth=0)] - if not region_line_tuples: - LOG.warning("Page '%s' contains no text lines", page_id) - for region_id, line in region_line_tuples: - line_image, line_xywh = self.workspace.image_from_segment(line, page_image, page_xywh, feature_filter='binarized') - line_image_bin = cv2pil(binarizer.run(image=pil2cv(line_image), use_patches=True)) - line_image_bin_path = self.workspace.save_image_file( - line_image_bin, - "%s_%s_%s.IMG-BIN" % (file_id, region_id, line.id), - page_id=input_file.pageId, - file_grp=self.output_file_grp) - line.add_AlternativeImage( - AlternativeImageType(filename=line_image_bin_path, comments='%s,binarized' % line_xywh['features'])) - - self.workspace.add_file( - ID=file_id, - file_grp=self.output_file_grp, - pageId=input_file.pageId, - mimetype=MIMETYPE_PAGE, - local_filename=join(self.output_file_grp, file_id + '.xml'), - content=to_xml(pcgts)) + return result @command() @ocrd_cli_options -def cli(*args, **kwargs): +def main(*args, **kwargs): return ocrd_cli_wrap_processor(SbbBinarizeProcessor, *args, **kwargs) From 9d61acf173ec1b26d71d147a08edc9b7177f9e49 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 02:02:30 +0200 Subject: [PATCH 350/412] simplify --- src/eynollah/eynollah.py | 6 ++---- src/eynollah/utils/__init__.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 68a9580..bfc93f0 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4084,10 +4084,8 @@ class Eynollah: ind_textline_inside_tr = list(range(len(contours[jj]))) index_textline_inside_textregion = index_textline_inside_textregion + ind_textline_inside_tr - #ind_ins = [0] * len(contours[jj]) + jj - ind_ins = np.zeros( len(contours[jj]) ) + jj - list_ind_ins = list(ind_ins) - indexes_of_textline_tot = indexes_of_textline_tot + list_ind_ins + ind_ins = [jj] * len(contours[jj]) + indexes_of_textline_tot = indexes_of_textline_tot + ind_ins M_main_tot = [cv2.moments(contours_txtline_of_all_textregions[j]) for j in range(len(contours_txtline_of_all_textregions))] diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index a67fc38..70216e1 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -237,10 +237,8 @@ def return_x_start_end_mothers_childs_and_type_of_reading_order( if len(remained_sep_indexes)>1: #print(np.array(remained_sep_indexes),'np.array(remained_sep_indexes)') #print(np.array(mother),'mother') - ##remained_sep_indexes_without_mother = remained_sep_indexes[mother==0] - ##remained_sep_indexes_with_child_without_mother = remained_sep_indexes[mother==0 & child==1] - remained_sep_indexes_without_mother=np.array(list(remained_sep_indexes))[np.array(mother)==0] - remained_sep_indexes_with_child_without_mother=np.array(list(remained_sep_indexes))[(np.array(mother)==0) & (np.array(child)==1)] + remained_sep_indexes_without_mother = remained_sep_indexes[mother==0] + remained_sep_indexes_with_child_without_mother = remained_sep_indexes[(mother==0) & (child==1)] #print(remained_sep_indexes_without_mother,'remained_sep_indexes_without_mother') #print(remained_sep_indexes_without_mother,'remained_sep_indexes_without_mother') From 4be89910a2a7a610e7f5a16d90f277cbe290a6bf Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 02:38:24 +0200 Subject: [PATCH 351/412] CLI: fix arg vs kwarg from merge --- src/eynollah/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index 4c9bb97..7dab4c7 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -271,7 +271,7 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ sys.exit(1) eynollah = Eynollah( model, - getLogger('Eynollah'), + logger=getLogger('Eynollah'), image_filename=image, overwrite=overwrite, dir_out=out, From 46618f422912034b0b9af5c873e5ed276217e444 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 14:11:50 +0200 Subject: [PATCH 352/412] allow more empty imports for optional dependencies --- src/eynollah/eynollah.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index bfc93f0..e74039d 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -7,6 +7,7 @@ document layout analysis (segmentation) with output in PAGE-XML """ from logging import Logger +from difflib import SequenceMatcher as sq import math import os import sys @@ -17,23 +18,34 @@ import warnings from functools import partial from pathlib import Path from multiprocessing import cpu_count -from loky import ProcessPoolExecutor import gc +import copy +import json + +from loky import ProcessPoolExecutor from PIL.Image import Image -from ocrd import OcrdPage -from ocrd_utils import getLogger +import xml.etree.ElementTree as ET import cv2 import numpy as np -import torch -from difflib import SequenceMatcher as sq +from scipy.signal import find_peaks +from scipy.ndimage import gaussian_filter1d +from numba import cuda + +from ocrd import OcrdPage +from ocrd_utils import getLogger + +try: + import torch +except ImportError: + torch = None +try: + import matplotlib.pyplot as plt +except ImportError: + plt = None try: from transformers import TrOCRProcessor, VisionEncoderDecoderModel except ImportError: TrOCRProcessor = VisionEncoderDecoderModel = None -from numba import cuda -import copy -from scipy.signal import find_peaks -from scipy.ndimage import gaussian_filter1d os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" #os.environ['CUDA_VISIBLE_DEVICES'] = '-1' @@ -45,12 +57,9 @@ from tensorflow.keras.models import load_model sys.stderr = stderr tf.get_logger().setLevel("ERROR") warnings.filterwarnings("ignore") -import matplotlib.pyplot as plt # use tf1 compatibility for keras backend from tensorflow.compat.v1.keras.backend import set_session from tensorflow.keras import layers -import json -import xml.etree.ElementTree as ET from tensorflow.keras.layers import StringLookup from .utils.contour import ( From 09248d48292cd890e4e18067043ef3113b376713 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 14:13:16 +0200 Subject: [PATCH 353/412] improve+extend makefile --- Makefile | 52 +++++++++++++++++++++++++++++++++----------------- pyproject.toml | 1 + 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 506fcf7..68c66b8 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,17 @@ -EYNOLLAH_MODELS ?= $(PWD)/models_eynollah -export EYNOLLAH_MODELS +PYTHON ?= python3 +PIP ?= pip3 # DOCKER_BASE_IMAGE = artefakt.dev.sbb.berlin:5000/sbb/ocrd_core:v2.68.0 -DOCKER_BASE_IMAGE = docker.io/ocrd/core:v2.68.0 +DOCKER_BASE_IMAGE = docker.io/ocrd/core-cuda-tf2:v3.3.0 DOCKER_TAG = ocrd/eynollah +#MODEL := 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' +#MODEL := 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' +MODEL := 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah.tar.gz' +#MODEL := 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' +#MODEL := 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' + +PYTEST_ARGS ?= # BEGIN-EVAL makefile-parser --make-help Makefile @@ -12,12 +19,19 @@ help: @echo "" @echo " Targets" @echo "" - @echo " models Download and extract models to $(PWD)/models_eynollah" - @echo " install Install with pip" + @echo " docker Build Docker image" + @echo " build Build Python source and binary distribution" + @echo " install Install package with pip" @echo " install-dev Install editable with pip" + @echo " deps-test Install test dependencies with pip" + @echo " models Download and extract models to $(CURDIR)/models_eynollah" + @echo " smoke-test Run simple CLI check" @echo " test Run unit tests" @echo "" @echo " Variables" + @echo " DOCKER_TAG Docker image tag for 'docker' [$(DOCKER_TAG)]" + @echo " PYTEST_ARGS pytest args for 'test' (Set to '-s' to see log output during test execution, '-vv' to see individual tests. [$(PYTEST_ARGS)]" + @echo " MODEL URL of 'models' archive to download for 'test' [$(MODEL)]" @echo "" # END-EVAL @@ -27,29 +41,32 @@ help: models: models_eynollah models_eynollah: models_eynollah.tar.gz - tar xf models_eynollah.tar.gz + tar zxf models_eynollah.tar.gz models_eynollah.tar.gz: - # wget 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' - # wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' - wget 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah.tar.gz' - # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' - # wget 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' + wget $(MODEL) + +build: + $(PIP) install build + $(PYTHON) -m build . # Install with pip install: - pip install . + $(PIP) install . # Install editable with pip install-dev: - pip install -e . + $(PIP) install -e . -smoke-test: - eynollah layout -i tests/resources/kant_aufklaerung_1784_0020.tif -o . -m $(PWD)/models_eynollah +deps-test: + $(PIP) install -r requirements-test.txt + +smoke-test: deps-test + eynollah layout -i tests/resources/kant_aufklaerung_1784_0020.tif -o . -m $(CURDIR)/models_eynollah # Run unit tests -test: - pytest tests +test: deps-test + EYNOLLAH_MODELS=$(CURDIR)/models_eynollah $(PYTHON) -m pytest tests --durations=0 --continue-on-collection-errors $(PYTEST_ARGS) # Build docker image docker: @@ -59,3 +76,4 @@ docker: --build-arg BUILD_DATE=$$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ -t $(DOCKER_TAG) . +.PHONY: models build install install-dev test smoke-test docker help diff --git a/pyproject.toml b/pyproject.toml index 66ba733..b1dea4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ Repository = "https://github.com/qurator-spk/eynollah.git" [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]} +optional-dependencies.test = {file = ["requirements-test.txt"]} [tool.setuptools.packages.find] where = ["src"] From 51e9bfd6d7b31d9c227aa0d582b08d7665e68fb4 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 14:14:08 +0200 Subject: [PATCH 354/412] improve+extend dockerfile --- Dockerfile | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6780bc2..5604842 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,23 +4,40 @@ FROM $DOCKER_BASE_IMAGE ARG VCS_REF ARG BUILD_DATE LABEL \ - maintainer="https://ocr-d.de/kontakt" \ + maintainer="https://ocr-d.de/en/contact" \ org.label-schema.vcs-ref=$VCS_REF \ org.label-schema.vcs-url="https://github.com/qurator-spk/eynollah" \ - org.label-schema.build-date=$BUILD_DATE + org.label-schema.build-date=$BUILD_DATE \ + org.opencontainers.image.vendor="DFG-Funded Initiative for Optical Character Recognition Development" \ + org.opencontainers.image.title="Eynollah" \ + org.opencontainers.image.description="" \ + org.opencontainers.image.source="https://github.com/qurator-spk/eynollah" \ + org.opencontainers.image.documentation="https://github.com/qurator-spk/eynollah/blob/${VCS_REF}/README.md" \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.base.name=ocrd/core-cuda-tf2 ENV DEBIAN_FRONTEND=noninteractive +# set proper locales ENV PYTHONIOENCODING=utf8 -ENV XDG_DATA_HOME=/usr/local/share +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 -WORKDIR /build-eynollah -COPY src/ ./src -COPY pyproject.toml . -COPY requirements.txt . -COPY README.md . -COPY Makefile . -RUN apt-get install -y --no-install-recommends g++ -RUN make install +# avoid HOME/.local/share (hard to predict USER here) +# so let XDG_DATA_HOME coincide with fixed system location +# (can still be overridden by derived stages) +ENV XDG_DATA_HOME /usr/local/share +# avoid the need for an extra volume for persistent resource user db +# (i.e. XDG_CONFIG_HOME/ocrd/resources.yml) +ENV XDG_CONFIG_HOME /usr/local/share/ocrd-resources + +WORKDIR /build/eynollah +COPY . . +COPY ocrd-tool.json . +# prepackage ocrd-tool.json as ocrd-all-tool.json +RUN ocrd ocrd-tool ocrd-tool.json dump-tools > $(dirname $(ocrd bashlib filename))/ocrd-all-tool.json +# install everything and reduce image size +RUN apt-get install -y --no-install-recommends g++ && make install && rm -rf /build/eynollah && apt-get remove -y --auto-remove g++ WORKDIR /data VOLUME /data From c01609ff4e91e59296254eb33f1de871b856ce85 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 15:57:22 +0200 Subject: [PATCH 355/412] allow even more empty imports for optional dependencies --- src/eynollah/plot.py | 7 +++++-- src/eynollah/utils/__init__.py | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/eynollah/plot.py b/src/eynollah/plot.py index b01fc04..412ae5a 100644 --- a/src/eynollah/plot.py +++ b/src/eynollah/plot.py @@ -1,5 +1,8 @@ -import matplotlib.pyplot as plt -import matplotlib.patches as mpatches +try: + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches +except ImportError: + plt = mpatches = None import numpy as np import os.path import cv2 diff --git a/src/eynollah/utils/__init__.py b/src/eynollah/utils/__init__.py index 70216e1..29b1273 100644 --- a/src/eynollah/utils/__init__.py +++ b/src/eynollah/utils/__init__.py @@ -1,13 +1,17 @@ +import time import math -import matplotlib.pyplot as plt +try: + import matplotlib.pyplot as plt +except ImportError: + plt = None import numpy as np from shapely import geometry import cv2 import imutils from scipy.signal import find_peaks from scipy.ndimage import gaussian_filter1d -import time + from .is_nan import isNaN from .contour import (contours_in_same_horizon, find_new_features_of_contours, From 722b5c6bf194248f608e3233d96cfbbe0f3da70f Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 15:58:12 +0200 Subject: [PATCH 356/412] add make variable EXTRAS for optional dependencies --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 68c66b8..08dd4e8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PYTHON ?= python3 PIP ?= pip3 +EXTRAS ?= # DOCKER_BASE_IMAGE = artefakt.dev.sbb.berlin:5000/sbb/ocrd_core:v2.68.0 DOCKER_BASE_IMAGE = docker.io/ocrd/core-cuda-tf2:v3.3.0 @@ -29,6 +30,7 @@ help: @echo " test Run unit tests" @echo "" @echo " Variables" + @echo " EXTRAS comma-separated list of features (like 'OCR,plotting') for 'install' [$(EXTRAS)]" @echo " DOCKER_TAG Docker image tag for 'docker' [$(DOCKER_TAG)]" @echo " PYTEST_ARGS pytest args for 'test' (Set to '-s' to see log output during test execution, '-vv' to see individual tests. [$(PYTEST_ARGS)]" @echo " MODEL URL of 'models' archive to download for 'test' [$(MODEL)]" @@ -52,11 +54,11 @@ build: # Install with pip install: - $(PIP) install . + $(PIP) install .$(and $(EXTRAS),[$(EXTRAS)]) # Install editable with pip install-dev: - $(PIP) install -e . + $(PIP) install -e .$(and $(EXTRAS),[$(EXTRAS)]) deps-test: $(PIP) install -r requirements-test.txt From ae066388ea55003d34364a471772f9695fbd319e Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 15:58:57 +0200 Subject: [PATCH 357/412] docker: no need for g++, but install w/ 'EXTRAS=OCR' --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5604842..177956d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ COPY ocrd-tool.json . # prepackage ocrd-tool.json as ocrd-all-tool.json RUN ocrd ocrd-tool ocrd-tool.json dump-tools > $(dirname $(ocrd bashlib filename))/ocrd-all-tool.json # install everything and reduce image size -RUN apt-get install -y --no-install-recommends g++ && make install && rm -rf /build/eynollah && apt-get remove -y --auto-remove g++ +RUN make install EXTRAS=OCR && rm -rf /build/eynollah WORKDIR /data VOLUME /data From f35f49376e3599f0ad88a771dbbd56f8f67f7bdd Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Mon, 31 Mar 2025 16:55:57 +0200 Subject: [PATCH 358/412] run CLI test in TMPDIR, add ocrd-test --- Makefile | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 08dd4e8..e49177b 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ help: @echo " deps-test Install test dependencies with pip" @echo " models Download and extract models to $(CURDIR)/models_eynollah" @echo " smoke-test Run simple CLI check" + @echo " ocrd-test Run OCR-D CLI check" @echo " test Run unit tests" @echo "" @echo " Variables" @@ -60,11 +61,26 @@ install: install-dev: $(PIP) install -e .$(and $(EXTRAS),[$(EXTRAS)]) -deps-test: +deps-test: models_eynollah $(PIP) install -r requirements-test.txt -smoke-test: deps-test - eynollah layout -i tests/resources/kant_aufklaerung_1784_0020.tif -o . -m $(CURDIR)/models_eynollah +smoke-test: TMPDIR != mktemp -d +smoke-test: tests/resources/kant_aufklaerung_1784_0020.tif deps-test + eynollah layout -i $< -o $(TMPDIR) -m $(CURDIR)/models_eynollah + fgrep -q http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15 $(TMPDIR)/$(basename $( Date: Mon, 31 Mar 2025 16:56:47 +0200 Subject: [PATCH 359/412] dockerfile: add smoke test --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 177956d..4785fc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,8 @@ COPY ocrd-tool.json . RUN ocrd ocrd-tool ocrd-tool.json dump-tools > $(dirname $(ocrd bashlib filename))/ocrd-all-tool.json # install everything and reduce image size RUN make install EXTRAS=OCR && rm -rf /build/eynollah +# smoke test +RUN eynollah --help WORKDIR /data VOLUME /data From 31aeb9629d0b19fea45c429321d3cfa0c0cd3110 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:16:17 +0200 Subject: [PATCH 360/412] Github Actions: free space more aggressively --- .github/workflows/test-eynollah.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 479c371..d584896 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -16,10 +16,17 @@ jobs: steps: - name: clean up run: | + df -h sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android sudo rm -rf /opt/ghc sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + docker rmi $(docker images --filter=reference="alpine:*" -q) + docker rmi $(docker images --filter=reference="debian:*" -q) + docker rmi $(docker images --filter=reference="node:*" -q) + df -h - uses: actions/checkout@v4 - uses: actions/cache@v4 id: model_cache From b1da0a332745314d2a36b4e0afa8d636bc8600c5 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 31 Mar 2025 18:43:14 +0200 Subject: [PATCH 361/412] In OCR, the predicted text is now drawn on the image, and the results are saved in a specified directory. This makes it easier to review the predicted output --- src/eynollah/cli.py | 16 ++++++++++- src/eynollah/eynollah.py | 62 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index c306ac5..369dc4c 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -334,6 +334,12 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ help="directory of xmls", type=click.Path(exists=True, file_okay=False), ) +@click.option( + "--dir_out_image_text", + "-doit", + help="directory of images with predicted text", + type=click.Path(exists=True, file_okay=False), +) @click.option( "--model", "-m", @@ -359,6 +365,12 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ is_flag=True, help="if this parameter set to true, cropped textline images will not be masked with textline contour.", ) +@click.option( + "--draw_texts_on_image", + "-dtoi/-ndtoi", + is_flag=True, + help="if this parameter set to true, the predicted texts will be displayed on an image.", +) @click.option( "--log_level", "-l", @@ -366,18 +378,20 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ help="Override log level globally to this", ) -def ocr(dir_in, out, dir_xmls, model, tr_ocr, export_textline_images_and_text, do_not_mask_with_textline_contour, log_level): +def ocr(dir_in, out, dir_xmls, dir_out_image_text, model, tr_ocr, export_textline_images_and_text, do_not_mask_with_textline_contour, draw_texts_on_image, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() eynollah_ocr = Eynollah_ocr( dir_xmls=dir_xmls, + dir_out_image_text=dir_out_image_text, dir_in=dir_in, dir_out=out, dir_models=model, tr_ocr=tr_ocr, export_textline_images_and_text=export_textline_images_and_text, do_not_mask_with_textline_contour=do_not_mask_with_textline_contour, + draw_texts_on_image=draw_texts_on_image, ) eynollah_ocr.run() diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 9ead53e..0b93085 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -22,7 +22,7 @@ from ocrd_utils import getLogger import cv2 import numpy as np from transformers import TrOCRProcessor -from PIL import Image +from PIL import Image, ImageDraw, ImageFont import torch from difflib import SequenceMatcher as sq from transformers import VisionEncoderDecoderModel @@ -4409,7 +4409,6 @@ class Eynollah: text_regions_p_1 ,erosion_hurts, polygons_lines_xml, textline_mask_tot_ea, img_bin_light = \ self.get_regions_light_v(img_res, is_image_enhanced, num_col_classifier) #print("text region early -2 in %.1fs", time.time() - t0) - if num_col_classifier == 1 or num_col_classifier ==2: if num_col_classifier == 1: img_w_new = 1000 @@ -4954,9 +4953,11 @@ class Eynollah_ocr: dir_xmls=None, dir_in=None, dir_out=None, + dir_out_image_text=None, tr_ocr=False, export_textline_images_and_text=False, do_not_mask_with_textline_contour=False, + draw_texts_on_image=False, logger=None, ): self.dir_in = dir_in @@ -4966,6 +4967,8 @@ class Eynollah_ocr: self.tr_ocr = tr_ocr self.export_textline_images_and_text = export_textline_images_and_text self.do_not_mask_with_textline_contour = do_not_mask_with_textline_contour + self.draw_texts_on_image = draw_texts_on_image + self.dir_out_image_text = dir_out_image_text if tr_ocr: self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") @@ -5083,6 +5086,23 @@ class Eynollah_ocr: return peaks_final else: return None + + # Function to fit text inside the given area + def fit_text_single_line(self, draw, text, font_path, max_width, max_height): + initial_font_size = 50 + font_size = initial_font_size + while font_size > 10: # Minimum font size + font = ImageFont.truetype(font_path, font_size) + text_bbox = draw.textbbox((0, 0), text, font=font) # Get text bounding box + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + if text_width <= max_width and text_height <= max_height: + return font # Return the best-fitting font + + font_size -= 2 # Reduce font size and retry + + return ImageFont.truetype(font_path, 10) # Smallest font fallback def return_textlines_split_if_needed(self, textline_image): @@ -5254,6 +5274,12 @@ class Eynollah_ocr: dir_xml = os.path.join(self.dir_xmls, file_name+'.xml') out_file_ocr = os.path.join(self.dir_out, file_name+'.xml') img = cv2.imread(dir_img) + + if self.draw_texts_on_image: + out_image_with_text = os.path.join(self.dir_out_image_text, file_name+'.png') + image_text = Image.new("RGB", (img.shape[1], img.shape[0]), "white") + draw = ImageDraw.Draw(image_text) + total_bb_coordinates = [] tree1 = ET.parse(dir_xml, parser = ET.XMLParser(encoding="utf-8")) root1=tree1.getroot() @@ -5283,6 +5309,9 @@ class Eynollah_ocr: x,y,w,h = cv2.boundingRect(textline_coords) + if self.draw_texts_on_image: + total_bb_coordinates.append([x,y,w,h]) + h2w_ratio = h/float(w) img_poly_on_img = np.copy(img) @@ -5359,6 +5388,35 @@ class Eynollah_ocr: extracted_texts_merged = [ind for ind in extracted_texts_merged if ind is not None] unique_cropped_lines_region_indexer = np.unique(cropped_lines_region_indexer) + + + if self.draw_texts_on_image: + + font_path = "NotoSans-Regular.ttf" # Make sure this file exists! + font = ImageFont.truetype(font_path, 40) + + for indexer_text, bb_ind in enumerate(total_bb_coordinates): + + + x_bb = bb_ind[0] + y_bb = bb_ind[1] + w_bb = bb_ind[2] + h_bb = bb_ind[3] + + font = self.fit_text_single_line(draw, extracted_texts_merged[indexer_text], font_path, w_bb, int(h_bb*0.4) ) + + ##draw.rectangle([x_bb, y_bb, x_bb + w_bb, y_bb + h_bb], outline="red", width=2) + + text_bbox = draw.textbbox((0, 0), extracted_texts_merged[indexer_text], font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + text_x = x_bb + (w_bb - text_width) // 2 # Center horizontally + text_y = y_bb + (h_bb - text_height) // 2 # Center vertically + + # Draw the text + draw.text((text_x, text_y), extracted_texts_merged[indexer_text], fill="black", font=font) + image_text.save(out_image_with_text) text_by_textregion = [] for ind in unique_cropped_lines_region_indexer: From 4de441eaaa4e212fff6436258deeb091180405bd Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Mon, 31 Mar 2025 21:28:05 +0200 Subject: [PATCH 362/412] OCR prediction is now enabled to integrate results from both RGB and binarized images or to be performed on each individually --- src/eynollah/cli.py | 16 ++++++++++- src/eynollah/eynollah.py | 62 +++++++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index 369dc4c..8bd5cf6 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -321,6 +321,12 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ help="directory of images", type=click.Path(exists=True, file_okay=False), ) +@click.option( + "--dir_in_bin", + "-dib", + help="directory of binarized images. This should be given if you want to do prediction based on both rgb and bin images. And all bin images are png files", + type=click.Path(exists=True, file_okay=False), +) @click.option( "--out", "-o", @@ -371,6 +377,12 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ is_flag=True, help="if this parameter set to true, the predicted texts will be displayed on an image.", ) +@click.option( + "--prediction_with_both_of_rgb_and_bin", + "-brb/-nbrb", + is_flag=True, + help="If this parameter is set to True, the prediction will be performed using both RGB and binary images. However, this does not necessarily improve results; it may be beneficial for certain document images.", +) @click.option( "--log_level", "-l", @@ -378,7 +390,7 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ help="Override log level globally to this", ) -def ocr(dir_in, out, dir_xmls, dir_out_image_text, model, tr_ocr, export_textline_images_and_text, do_not_mask_with_textline_contour, draw_texts_on_image, log_level): +def ocr(dir_in, dir_in_bin, out, dir_xmls, dir_out_image_text, model, tr_ocr, export_textline_images_and_text, do_not_mask_with_textline_contour, draw_texts_on_image, prediction_with_both_of_rgb_and_bin, log_level): if log_level: setOverrideLogLevel(log_level) initLogging() @@ -386,12 +398,14 @@ def ocr(dir_in, out, dir_xmls, dir_out_image_text, model, tr_ocr, export_textlin dir_xmls=dir_xmls, dir_out_image_text=dir_out_image_text, dir_in=dir_in, + dir_in_bin=dir_in_bin, dir_out=out, dir_models=model, tr_ocr=tr_ocr, export_textline_images_and_text=export_textline_images_and_text, do_not_mask_with_textline_contour=do_not_mask_with_textline_contour, draw_texts_on_image=draw_texts_on_image, + prediction_with_both_of_rgb_and_bin=prediction_with_both_of_rgb_and_bin, ) eynollah_ocr.run() diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 0b93085..1534e7e 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4952,15 +4952,18 @@ class Eynollah_ocr: dir_models, dir_xmls=None, dir_in=None, + dir_in_bin=None, dir_out=None, dir_out_image_text=None, tr_ocr=False, export_textline_images_and_text=False, do_not_mask_with_textline_contour=False, draw_texts_on_image=False, + prediction_with_both_of_rgb_and_bin=False, logger=None, ): self.dir_in = dir_in + self.dir_in_bin = dir_in_bin self.dir_out = dir_out self.dir_xmls = dir_xmls self.dir_models = dir_models @@ -4969,6 +4972,7 @@ class Eynollah_ocr: self.do_not_mask_with_textline_contour = do_not_mask_with_textline_contour self.draw_texts_on_image = draw_texts_on_image self.dir_out_image_text = dir_out_image_text + self.prediction_with_both_of_rgb_and_bin = prediction_with_both_of_rgb_and_bin if tr_ocr: self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-printed") self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") @@ -4977,7 +4981,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_step_150000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_step_50000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5104,15 +5108,20 @@ class Eynollah_ocr: return ImageFont.truetype(font_path, 10) # Smallest font fallback - def return_textlines_split_if_needed(self, textline_image): + def return_textlines_split_if_needed(self, textline_image, textline_image_bin): split_point = self.return_start_and_end_of_common_text_of_textline_ocr_without_common_section(textline_image) if split_point: image1 = textline_image[:, :split_point,:]# image.crop((0, 0, width2, height)) image2 = textline_image[:, split_point:,:]#image.crop((width1, 0, width, height)) - return [image1, image2] + if self.prediction_with_both_of_rgb_and_bin: + image1_bin = textline_image_bin[:, :split_point,:]# image.crop((0, 0, width2, height)) + image2_bin = textline_image_bin[:, split_point:,:]#image.crop((width1, 0, width, height)) + return [image1, image2], [image1_bin, image2_bin] + else: + return [image1, image2], None else: - return None + return None, None def preprocess_and_resize_image_for_ocrcnn_model(self, img, image_height, image_width): ratio = image_height /float(img.shape[0]) w_ratio = int(ratio * img.shape[1]) @@ -5123,7 +5132,7 @@ class Eynollah_ocr: img = resize_image(img, image_height, width_new) img_fin = np.ones((image_height, image_width, 3))*255 - img_fin[:,:width_new,:] = img[:,:,:] + img_fin[:,:+width_new,:] = img[:,:,:] img_fin = img_fin / 255. return img_fin @@ -5183,7 +5192,7 @@ class Eynollah_ocr: cropped_lines.append(img_crop) cropped_lines_meging_indexing.append(0) else: - splited_images = self.return_textlines_split_if_needed(img_crop) + splited_images, _ = self.return_textlines_split_if_needed(img_crop, None) #print(splited_images) if splited_images: cropped_lines.append(splited_images[0]) @@ -5274,6 +5283,10 @@ class Eynollah_ocr: dir_xml = os.path.join(self.dir_xmls, file_name+'.xml') out_file_ocr = os.path.join(self.dir_out, file_name+'.xml') img = cv2.imread(dir_img) + if self.prediction_with_both_of_rgb_and_bin: + cropped_lines_bin = [] + dir_img_bin = os.path.join(self.dir_in_bin, file_name+'.png') + img_bin = cv2.imread(dir_img_bin) if self.draw_texts_on_image: out_image_with_text = os.path.join(self.dir_out_image_text, file_name+'.png') @@ -5315,6 +5328,10 @@ class Eynollah_ocr: h2w_ratio = h/float(w) img_poly_on_img = np.copy(img) + if self.prediction_with_both_of_rgb_and_bin: + img_poly_on_img_bin = np.copy(img_bin) + img_crop_bin = img_poly_on_img_bin[y:y+h, x:x+w, :] + mask_poly = np.zeros(img.shape) mask_poly = cv2.fillPoly(mask_poly, pts=[textline_coords], color=(1, 1, 1)) @@ -5322,14 +5339,22 @@ class Eynollah_ocr: img_crop = img_poly_on_img[y:y+h, x:x+w, :] if not self.do_not_mask_with_textline_contour: img_crop[mask_poly==0] = 255 + if self.prediction_with_both_of_rgb_and_bin: + img_crop_bin[mask_poly==0] = 255 if not self.export_textline_images_and_text: if h2w_ratio > 0.1: img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) cropped_lines.append(img_fin) cropped_lines_meging_indexing.append(0) + if self.prediction_with_both_of_rgb_and_bin: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop_bin, image_height, image_width) + cropped_lines_bin.append(img_fin) else: - splited_images = self.return_textlines_split_if_needed(img_crop) + if self.prediction_with_both_of_rgb_and_bin: + splited_images, splited_images_bin = self.return_textlines_split_if_needed(img_crop, img_crop_bin) + else: + splited_images, splited_images_bin = self.return_textlines_split_if_needed(img_crop, None) if splited_images: img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images[0], image_height, image_width) cropped_lines.append(img_fin) @@ -5338,10 +5363,21 @@ class Eynollah_ocr: cropped_lines.append(img_fin) cropped_lines_meging_indexing.append(-1) + + if self.prediction_with_both_of_rgb_and_bin: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images_bin[0], image_height, image_width) + cropped_lines_bin.append(img_fin) + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(splited_images_bin[1], image_height, image_width) + cropped_lines_bin.append(img_fin) + else: img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop, image_height, image_width) cropped_lines.append(img_fin) cropped_lines_meging_indexing.append(0) + + if self.prediction_with_both_of_rgb_and_bin: + img_fin = self.preprocess_and_resize_image_for_ocrcnn_model(img_crop_bin, image_height, image_width) + cropped_lines_bin.append(img_fin) if self.export_textline_images_and_text: if child_textlines.tag.endswith("TextEquiv"): @@ -5370,14 +5406,26 @@ class Eynollah_ocr: imgs = cropped_lines[n_start:] imgs = np.array(imgs) imgs = imgs.reshape(imgs.shape[0], image_height, image_width, 3) + if self.prediction_with_both_of_rgb_and_bin: + imgs_bin = cropped_lines_bin[n_start:] + imgs_bin = np.array(imgs_bin) + imgs_bin = imgs_bin.reshape(imgs_bin.shape[0], image_height, image_width, 3) else: n_start = i*b_s n_end = (i+1)*b_s imgs = cropped_lines[n_start:n_end] imgs = np.array(imgs).reshape(b_s, image_height, image_width, 3) + if self.prediction_with_both_of_rgb_and_bin: + imgs_bin = cropped_lines_bin[n_start:n_end] + imgs_bin = np.array(imgs_bin).reshape(b_s, image_height, image_width, 3) + preds = self.prediction_model.predict(imgs, verbose=0) + if self.prediction_with_both_of_rgb_and_bin: + preds_bin = self.prediction_model.predict(imgs_bin, verbose=0) + preds = (preds + preds_bin) / 2. + pred_texts = self.decode_batch_predictions(preds) for ib in range(imgs.shape[0]): From 45e3ab9692e83efb1d8bb64d261f6b87122e214f Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:23:53 +0200 Subject: [PATCH 363/412] Github Actions: free space: all existing Docker images --- .github/workflows/test-eynollah.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index d584896..05d1274 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -23,9 +23,7 @@ jobs: sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" df -h - docker rmi $(docker images --filter=reference="alpine:*" -q) - docker rmi $(docker images --filter=reference="debian:*" -q) - docker rmi $(docker images --filter=reference="node:*" -q) + docker rmi $(docker images -q) df -h - uses: actions/checkout@v4 - uses: actions/cache@v4 From df3510750c6946bb2b17242fc82ac7dc093d23c8 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Tue, 1 Apr 2025 00:28:16 +0200 Subject: [PATCH 364/412] Github Actions CI: no more Docker clean or build --- .github/workflows/test-eynollah.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 05d1274..296d9c9 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -23,8 +23,6 @@ jobs: sudo rm -rf "/usr/local/share/boost" sudo rm -rf "$AGENT_TOOLSDIRECTORY" df -h - docker rmi $(docker images -q) - df -h - uses: actions/checkout@v4 - uses: actions/cache@v4 id: model_cache @@ -41,9 +39,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[OCR,plotting] - pip install -r requirements-test.txt + make install EXTRAS=OCR,plotting + make deps-test - name: Test with pytest run: make test - - name: Test docker build - run: make docker From 95a681aa8c46c67f2ca8abb377b39f56952fb6a8 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 01:27:10 +0200 Subject: [PATCH 365/412] add Continuous Deployment via Dockerhub and GHCR --- .dockerignore | 6 ++++ .github/workflows/build-docker.yml | 44 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-docker.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..562fb6f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +tests +dist +build +env* +*.egg-info +models_eynollah* diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml new file mode 100644 index 0000000..d77958b --- /dev/null +++ b/.github/workflows/build-docker.yml @@ -0,0 +1,44 @@ +name: CD + +on: + push: + branches: [ "master" ] + workflow_dispatch: # run manually + +jobs: + + build: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # we need tags for docker version tagging + fetch-tags: true + fetch-depth: 0 + - # Activate cache export feature to reduce build time of images + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERIO_USERNAME }} + password: ${{ secrets.DOCKERIO_PASSWORD }} + - name: Build the Docker image + # build both tags at the same time + run: make docker DOCKER_TAG="docker.io/ocrd/eynollah -t ghcr.io/qurator-spk/eynollah" + - name: Test the Docker image + run: docker run --rm ocrd/eynollah ocrd-eynollah-segment -h + - name: Push to Dockerhub + run: docker push docker.io/ocrd/eynollah + - name: Push to Github Container Registry + run: docker push ghcr.io/qurator-spk/eynollah From 515b4023f6b20f78088e15367f7d8931ddcee4e3 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 10:54:36 +0200 Subject: [PATCH 366/412] sbb_binarize: fix missing reference --- src/eynollah/ocrd_cli_binarization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/eynollah/ocrd_cli_binarization.py b/src/eynollah/ocrd_cli_binarization.py index 6bc4b74..848bbac 100644 --- a/src/eynollah/ocrd_cli_binarization.py +++ b/src/eynollah/ocrd_cli_binarization.py @@ -83,7 +83,7 @@ class SbbBinarizeProcessor(Processor): for region in regions: region_image, region_xywh = self.workspace.image_from_segment( region, page_image, page_xywh, feature_filter='binarized') - region_image_bin = cv2pil(binarizer.run(image=pil2cv(region_image), use_patches=True)) + region_image_bin = cv2pil(self.binarizer.run(image=pil2cv(region_image), use_patches=True)) # update PAGE (reference the image file): region_image_ref = AlternativeImageType(comments=region_xywh['features'] + ',binarized') region.add_AlternativeImage(region_image_ref) @@ -95,7 +95,7 @@ class SbbBinarizeProcessor(Processor): self.logger.warning("Page '%s' contains no text lines", page_id) for line in lines: line_image, line_xywh = self.workspace.image_from_segment(line, page_image, page_xywh, feature_filter='binarized') - line_image_bin = cv2pil(binarizer.run(image=pil2cv(line_image), use_patches=True)) + line_image_bin = cv2pil(self.binarizer.run(image=pil2cv(line_image), use_patches=True)) # update PAGE (reference the image file): line_image_ref = AlternativeImageType(comments=line_xywh['features'] + ',binarized') line.add_AlternativeImage(region_image_ref) From 91b2201b07b4a2c3860f17fc66789c18d60866b1 Mon Sep 17 00:00:00 2001 From: vahidrezanezhad Date: Tue, 1 Apr 2025 10:55:40 +0200 Subject: [PATCH 367/412] cnnrnn Ocr: width of input textline image can not be zero! --- src/eynollah/eynollah.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 1534e7e..436ce84 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -4981,7 +4981,7 @@ class Eynollah_ocr: self.model_ocr.to(self.device) else: - self.model_ocr_dir = dir_models + "/model_step_50000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" + self.model_ocr_dir = dir_models + "/model_step_150000_ocr"#"/model_0_ocr_cnnrnn"#"/model_23_ocr_cnnrnn" model_ocr = load_model(self.model_ocr_dir , compile=False) self.prediction_model = tf.keras.models.Model( @@ -5125,10 +5125,14 @@ class Eynollah_ocr: def preprocess_and_resize_image_for_ocrcnn_model(self, img, image_height, image_width): ratio = image_height /float(img.shape[0]) w_ratio = int(ratio * img.shape[1]) + if w_ratio <= image_width: width_new = w_ratio else: width_new = image_width + + if width_new == 0: + width_new = img.shape[1] img = resize_image(img, image_height, width_new) img_fin = np.ones((image_height, image_width, 3))*255 From 250fc02606466574a3ed137d1ed81ee0aebedb8a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 11:13:04 +0200 Subject: [PATCH 368/412] add tests for binarization, remove dependency on deps-test --- Makefile | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index e49177b..cbb0659 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,13 @@ EXTRAS ?= DOCKER_BASE_IMAGE = docker.io/ocrd/core-cuda-tf2:v3.3.0 DOCKER_TAG = ocrd/eynollah -#MODEL := 'https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz' -#MODEL := 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz' -MODEL := 'https://qurator-data.de/eynollah/2022-04-05/models_eynollah.tar.gz' -#MODEL := 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz' -#MODEL := 'https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz' +#SEG_MODEL := https://qurator-data.de/eynollah/2021-04-25/models_eynollah.tar.gz +#SEG_MODEL := https://qurator-data.de/eynollah/2022-04-05/models_eynollah_renamed.tar.gz +SEG_MODEL := https://qurator-data.de/eynollah/2022-04-05/models_eynollah.tar.gz +#SEG_MODEL := https://github.com/qurator-spk/eynollah/releases/download/v0.3.0/models_eynollah.tar.gz +#SEG_MODEL := https://github.com/qurator-spk/eynollah/releases/download/v0.3.1/models_eynollah.tar.gz + +BIN_MODEL := https://github.com/qurator-spk/sbb_binarization/releases/download/v0.0.11/saved_model_2021_03_09.zip PYTEST_ARGS ?= @@ -34,20 +36,29 @@ help: @echo " EXTRAS comma-separated list of features (like 'OCR,plotting') for 'install' [$(EXTRAS)]" @echo " DOCKER_TAG Docker image tag for 'docker' [$(DOCKER_TAG)]" @echo " PYTEST_ARGS pytest args for 'test' (Set to '-s' to see log output during test execution, '-vv' to see individual tests. [$(PYTEST_ARGS)]" - @echo " MODEL URL of 'models' archive to download for 'test' [$(MODEL)]" + @echo " SEG_MODEL URL of 'models' archive to download for segmentation 'test' [$(SEG_MODEL)]" + @echo " BIN_MODEL URL of 'models' archive to download for binarization 'test' [$(BIN_MODEL)]" @echo "" # END-EVAL # Download and extract models to $(PWD)/models_eynollah -models: models_eynollah +models: models_eynollah default-2021-03-09 models_eynollah: models_eynollah.tar.gz tar zxf models_eynollah.tar.gz models_eynollah.tar.gz: - wget $(MODEL) + wget $(SEG_MODEL) + +default-2021-03-09: $(notdir $(BIN_MODEL)) + unzip $(notdir $(BIN_MODEL)) + mkdir $@ + mv $(basename $(notdir $(BIN_MODEL))) $@ + +$(notdir $(BIN_MODEL)): + wget $(BIN_MODEL) build: $(PIP) install build @@ -65,14 +76,17 @@ deps-test: models_eynollah $(PIP) install -r requirements-test.txt smoke-test: TMPDIR != mktemp -d -smoke-test: tests/resources/kant_aufklaerung_1784_0020.tif deps-test +smoke-test: tests/resources/kant_aufklaerung_1784_0020.tif eynollah layout -i $< -o $(TMPDIR) -m $(CURDIR)/models_eynollah fgrep -q http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15 $(TMPDIR)/$(basename $( Date: Tue, 1 Apr 2025 11:13:16 +0200 Subject: [PATCH 369/412] CI: run CLI tests, too --- .github/workflows/test-eynollah.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index 296d9c9..c9d6d86 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -43,3 +43,7 @@ jobs: make deps-test - name: Test with pytest run: make test + - name: Test standalone CLI + run: make smoke-test + - name: Test OCR-D CLI + run: make ocrd-test From 9dc33db108f59795e1ef730f3eccf1dd007b4f77 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 11:36:56 +0200 Subject: [PATCH 370/412] CI: add binarization models to cache --- .github/workflows/test-eynollah.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-eynollah.yml b/.github/workflows/test-eynollah.yml index c9d6d86..59503aa 100644 --- a/.github/workflows/test-eynollah.yml +++ b/.github/workflows/test-eynollah.yml @@ -25,12 +25,17 @@ jobs: df -h - uses: actions/checkout@v4 - uses: actions/cache@v4 - id: model_cache + id: seg_model_cache with: path: models_eynollah key: ${{ runner.os }}-models + - uses: actions/cache@v4 + id: bin_model_cache + with: + path: default-2021-03-09 + key: ${{ runner.os }}-modelbin - name: Download models - if: steps.model_cache.outputs.cache-hit != 'true' + if: steps.seg_model_cache.outputs.cache-hit != 'true' || steps.bin_model_cache.outputs.cache-hit != 'true' run: make models - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 From ffeb4a343d92365c36e57a7599adbe2c6d2b0e3e Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 13:00:41 +0200 Subject: [PATCH 371/412] Eynollah: remove useless 'pcgts' attr --- src/eynollah/eynollah.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index e74039d..42bbf31 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -257,7 +257,6 @@ class Eynollah: self.num_col_lower = int(num_col_lower) else: self.num_col_lower = num_col_lower - self.pcgts = pcgts if not dir_in: self.plotter = None if not enable_plotting else EynollahPlotter( dir_out=self.dir_out, @@ -407,8 +406,7 @@ class Eynollah: dir_out=self.dir_out, image_filename=self.image_filename, curved_line=self.curved_line, - textline_light = self.textline_light, - pcgts=self.pcgts) + textline_light = self.textline_light) def imread(self, grayscale=False, uint8=True): key = 'img' From dd51f900b9abf12b29d73bc34b46d6f11e9f1d58 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 13:02:30 +0200 Subject: [PATCH 372/412] OCR-D: init Eynollah in 'setup', re-use instance for each page via non-public API --- src/eynollah/processor.py | 57 ++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index b5b9cb2..ed409f4 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -2,7 +2,7 @@ from typing import Optional from ocrd_models import OcrdPage from ocrd import Processor, OcrdPageResult -from .eynollah import Eynollah +from .eynollah import Eynollah, EynollahXmlWriter class EynollahProcessor(Processor): # already employs background CPU multiprocessing per page @@ -14,11 +14,32 @@ class EynollahProcessor(Processor): return 'ocrd-eynollah-segment' def setup(self) -> None: - # for caching models - self.models = None if self.parameter['textline_light'] and not self.parameter['light_version']: raise ValueError("Error: You set parameter 'textline_light' to enable light textline detection, " "but parameter 'light_version' is not enabled") + self.eynollah = Eynollah( + self.resolve_resource(self.parameter['models']), + logger=self.logger, + allow_enhancement=self.parameter['allow_enhancement'], + curved_line=self.parameter['curved_line'], + right2left=self.parameter['right_to_left'], + ignore_page_extraction=self.parameter['ignore_page_extraction'], + light_version=self.parameter['light_version'], + textline_light=self.parameter['textline_light'], + full_layout=self.parameter['full_layout'], + allow_scaling=self.parameter['allow_scaling'], + headers_off=self.parameter['headers_off'], + tables=self.parameter['tables'], + override_dpi=self.parameter['dpi'], + # trick Eynollah to do init independent of an image + dir_in="." + ) + self.eynollah.dir_in = None + self.eynollah.plotter = None + + def shutdown(self): + if hasattr(self, 'eynollah'): + del self.eynollah def process_page_pcgts(self, *input_pcgts: Optional[OcrdPage], page_id: Optional[str] = None) -> OcrdPageResult: """ @@ -60,27 +81,15 @@ class EynollahProcessor(Processor): image_filename = "dummy" # will be replaced by ocrd.Processor.process_page_file result.images.append(OcrdPageResultImage(page_image, '.IMG', page)) # mark as new original # FIXME: mask out already existing regions (incremental segmentation) - eynollah = Eynollah( - self.resolve_resource(self.parameter['models']), - logger=self.logger, - allow_enhancement=self.parameter['allow_enhancement'], - curved_line=self.parameter['curved_line'], - right2left=self.parameter['right_to_left'], - ignore_page_extraction=self.parameter['ignore_page_extraction'], - light_version=self.parameter['light_version'], - textline_light=self.parameter['textline_light'], - full_layout=self.parameter['full_layout'], - allow_scaling=self.parameter['allow_scaling'], - headers_off=self.parameter['headers_off'], - tables=self.parameter['tables'], - override_dpi=self.parameter['dpi'], - pcgts=pcgts, - image_filename=image_filename, + self.eynollah.image_filename = image_filename + self.eynollah._imgs = self.eynollah._cache_images( image_pil=page_image ) - if self.models is not None: - # reuse loaded models from previous page - eynollah.models = self.models - eynollah.run() - self.models = eynollah.models + self.eynollah.writer = EynollahXmlWriter( + dir_out=None, + image_filename=image_filename, + curved_line=self.eynollah.curved_line, + textline_light=self.eynollah.textline_light, + pcgts=pcgts) + self.eynollah.run() return result From ab3da175471d5ff3717f80ce5fe146d408120f51 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky <38561704+bertsky@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:13:28 +0200 Subject: [PATCH 373/412] Update requirements.txt Co-authored-by: Konstantin Baierer --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 280eadd..2e1fc79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # ocrd includes opencv, numpy, shapely, click -ocrd >= 3.1.0 +ocrd >= 3.3.0 numpy <1.24.0 scikit-learn >= 0.23.2 tensorflow < 2.13 From 1a0a1cb00b3820854f725a49bb35cd4c3cb42a6a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 21:15:41 +0200 Subject: [PATCH 374/412] remove session methods and redundant model loaders --- src/eynollah/eynollah.py | 198 ++++++++++----------------------------- 1 file changed, 51 insertions(+), 147 deletions(-) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 42bbf31..35f7898 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -323,50 +323,52 @@ class Eynollah: self.model_textline_dir = dir_models + "/modelens_textline_0_1__2_4_16092024" if self.ocr: self.model_ocr_dir = dir_models + "/trocr_model_ens_of_3_checkpoints_201124" - if self.tables: if self.light_version: self.model_table_dir = dir_models + "/modelens_table_0t4_201124" else: self.model_table_dir = dir_models + "/eynollah-tables_20210319" - - self.models = {} - - if dir_in: - # as in start_new_session: - config = tf.compat.v1.ConfigProto() - config.gpu_options.allow_growth = True - session = tf.compat.v1.Session(config=config) - set_session(session) - - self.model_page = self.our_load_model(self.model_page_dir) - self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) - self.model_bin = self.our_load_model(self.model_dir_of_binarization) - if self.extract_only_images: - self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) + + # #gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) + # #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) + # #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) + # config = tf.compat.v1.ConfigProto() + # config.gpu_options.allow_growth = True + # #session = tf.InteractiveSession() + # session = tf.compat.v1.Session(config=config) + # set_session(session) + try: + for device in tf.config.list_physical_devices('GPU'): + tf.config.experimental.set_memory_growth(device, True) + except: + self.logger.warning("no GPU device available") + + self.model_page = self.our_load_model(self.model_page_dir) + self.model_classifier = self.our_load_model(self.model_dir_of_col_classifier) + self.model_bin = self.our_load_model(self.model_dir_of_binarization) + if self.extract_only_images: + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light_only_images_extraction) + else: + self.model_textline = self.our_load_model(self.model_textline_dir) + if self.light_version: + self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) + self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) else: - self.model_textline = self.our_load_model(self.model_textline_dir) - if self.light_version: - self.model_region = self.our_load_model(self.model_region_dir_p_ens_light) - self.model_region_1_2 = self.our_load_model(self.model_region_dir_p_1_2_sp_np) - else: - self.model_region = self.our_load_model(self.model_region_dir_p_ens) - self.model_region_p2 = self.our_load_model(self.model_region_dir_p2) - self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) - ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) - self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) - self.model_region_fl = self.our_load_model(self.model_region_dir_fully) - if self.reading_order_machine_based: - self.model_reading_order = self.our_load_model(self.model_reading_order_dir) - if self.ocr: - self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) - self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - #("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") - self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten") - if self.tables: - self.model_table = self.our_load_model(self.model_table_dir) - - self.ls_imgs = os.listdir(self.dir_in) + self.model_region = self.our_load_model(self.model_region_dir_p_ens) + self.model_region_p2 = self.our_load_model(self.model_region_dir_p2) + self.model_enhancement = self.our_load_model(self.model_dir_of_enhancement) + ###self.model_region_fl_new = self.our_load_model(self.model_region_dir_fully_new) + self.model_region_fl_np = self.our_load_model(self.model_region_dir_fully_np) + self.model_region_fl = self.our_load_model(self.model_region_dir_fully) + if self.reading_order_machine_based: + self.model_reading_order = self.our_load_model(self.model_reading_order_dir) + if self.ocr: + self.model_ocr = VisionEncoderDecoderModel.from_pretrained(self.model_ocr_dir) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + #("microsoft/trocr-base-printed")#("microsoft/trocr-base-handwritten") + self.processor = TrOCRProcessor.from_pretrained("microsoft/trocr-base-handwritten") + if self.tables: + self.model_table = self.our_load_model(self.model_table_dir) def _cache_images(self, image_filename=None, image_pil=None): ret = {} @@ -421,8 +423,6 @@ class Eynollah: def predict_enhancement(self, img): self.logger.debug("enter predict_enhancement") - if not self.dir_in: - self.model_enhancement, _ = self.start_new_session_and_model(self.model_dir_of_enhancement) img_height_model = self.model_enhancement.layers[-1].output_shape[1] img_width_model = self.model_enhancement.layers[-1].output_shape[2] @@ -619,9 +619,6 @@ class Eynollah: img = self.imread() _, page_coord = self.early_page_for_num_of_column_classification(img) - - if not self.dir_in: - self.model_classifier, _ = self.start_new_session_and_model(self.model_dir_of_col_classifier) if self.input_binary: img_in = np.copy(img) @@ -662,9 +659,6 @@ class Eynollah: self.logger.info("Detected %s DPI", dpi) if self.input_binary: img = self.imread() - if not self.dir_in: - self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) - prediction_bin = self.do_prediction(True, img, self.model_bin, n_batch_inference=5) prediction_bin = 255 * (prediction_bin[:,:,0]==0) prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2).astype(np.uint8) @@ -673,17 +667,14 @@ class Eynollah: else: img = self.imread() img_bin = None - + width_early = img.shape[1] t1 = time.time() _, page_coord = self.early_page_for_num_of_column_classification(img_bin) - + self.image_page_org_size = img[page_coord[0] : page_coord[1], page_coord[2] : page_coord[3], :] self.page_coord = page_coord - - if not self.dir_in: - self.model_classifier, _ = self.start_new_session_and_model(self.model_dir_of_col_classifier) - + if self.num_col_upper and not self.num_col_lower: num_col = self.num_col_upper label_p_pred = [np.ones(6)] @@ -823,43 +814,6 @@ class Eynollah: self.writer.height_org = self.height_org self.writer.width_org = self.width_org - def start_new_session_and_model_old(self, model_dir): - self.logger.debug("enter start_new_session_and_model (model_dir=%s)", model_dir) - config = tf.ConfigProto() - config.gpu_options.allow_growth = True - - session = tf.InteractiveSession() - model = load_model(model_dir, compile=False) - - return model, session - - def start_new_session_and_model(self, model_dir): - self.logger.debug("enter start_new_session_and_model (model_dir=%s)", model_dir) - #gpu_options = tf.compat.v1.GPUOptions(allow_growth=True) - #gpu_options = tf.compat.v1.GPUOptions(per_process_gpu_memory_fraction=7.7, allow_growth=True) - #session = tf.compat.v1.Session(config=tf.compat.v1.ConfigProto(gpu_options=gpu_options)) - physical_devices = tf.config.list_physical_devices('GPU') - try: - for device in physical_devices: - tf.config.experimental.set_memory_growth(device, True) - except: - self.logger.warning("no GPU device available") - - if model_dir.endswith('.h5') and Path(model_dir[:-3]).exists(): - # prefer SavedModel over HDF5 format if it exists - model_dir = model_dir[:-3] - if model_dir in self.models: - model = self.models[model_dir] - else: - try: - model = load_model(model_dir, compile=False) - except: - model = load_model(model_dir , compile=False, custom_objects={ - "PatchEncoder": PatchEncoder, "Patches": Patches}) - self.models[model_dir] = model - - return model, None - def do_prediction( self, patches, img, model, n_batch_inference=1, marginal_of_patch_percent=0.1, @@ -1397,9 +1351,6 @@ class Eynollah: self.logger.debug("enter extract_page") cont_page = [] if not self.ignore_page_extraction: - if not self.dir_in: - self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) - img = cv2.GaussianBlur(self.image, (5, 5), 0) img_page_prediction = self.do_prediction(False, img, self.model_page) imgray = cv2.cvtColor(img_page_prediction, cv2.COLOR_BGR2GRAY) @@ -1447,8 +1398,6 @@ class Eynollah: img = np.copy(img_bin).astype(np.uint8) else: img = self.imread() - if not self.dir_in: - self.model_page, _ = self.start_new_session_and_model(self.model_page_dir) img = cv2.GaussianBlur(img, (5, 5), 0) img_page_prediction = self.do_prediction(False, img, self.model_page) @@ -1476,11 +1425,6 @@ class Eynollah: self.logger.debug("enter extract_text_regions") img_height_h = img.shape[0] img_width_h = img.shape[1] - if not self.dir_in: - if patches: - self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) - else: - self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) model_region = self.model_region_fl if patches else self.model_region_fl_np if self.light_version: @@ -1512,11 +1456,6 @@ class Eynollah: self.logger.debug("enter extract_text_regions") img_height_h = img.shape[0] img_width_h = img.shape[1] - if not self.dir_in: - if patches: - self.model_region_fl, _ = self.start_new_session_and_model(self.model_region_dir_fully) - else: - self.model_region_fl_np, _ = self.start_new_session_and_model(self.model_region_dir_fully_np) model_region = self.model_region_fl if patches else self.model_region_fl_np if not patches: @@ -1647,8 +1586,6 @@ class Eynollah: def textline_contours(self, img, use_patches, scaler_h, scaler_w, num_col_classifier=None): self.logger.debug('enter textline_contours') - if not self.dir_in: - self.model_textline, _ = self.start_new_session_and_model(self.model_textline_dir) #img = img.astype(np.uint8) img_org = np.copy(img) @@ -1750,9 +1687,6 @@ class Eynollah: img_h_new = int(img.shape[0] / float(img.shape[1]) * img_w_new) img_resized = resize_image(img,img_h_new, img_w_new ) - if not self.dir_in: - self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light_only_images_extraction) - prediction_regions_org = self.do_prediction_new_concept(True, img_resized, self.model_region) prediction_regions_org = resize_image(prediction_regions_org,img_height_h, img_width_h ) @@ -1841,7 +1775,6 @@ class Eynollah: img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - #model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) #print(num_col_classifier,'num_col_classifier') if num_col_classifier == 1: @@ -1864,8 +1797,6 @@ class Eynollah: #if self.input_binary: #img_bin = np.copy(img_resized) ###if (not self.input_binary and self.full_layout) or (not self.input_binary and num_col_classifier >= 30): - ###if not self.dir_in: - ###self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) ###prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) ####print("inside bin ", time.time()-t_bin) @@ -1881,8 +1812,6 @@ class Eynollah: ###else: ###img_bin = np.copy(img_resized) if self.ocr and not self.input_binary: - if not self.dir_in: - self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_resized, self.model_bin, n_batch_inference=5) prediction_bin = 255 * (prediction_bin[:,:,0] == 0) prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) @@ -1905,12 +1834,7 @@ class Eynollah: #plt.show() if not skip_layout_and_reading_order: #print("inside 2 ", time.time()-t_in) - if not self.dir_in: - self.model_region_1_2, _ = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) - ##self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens_light) - if num_col_classifier == 1 or num_col_classifier == 2: - model_region, session_region = self.start_new_session_and_model(self.model_region_dir_p_1_2_sp_np) if self.image_org.shape[0]/self.image_org.shape[1] > 2.5: self.logger.debug("resized to %dx%d for %d cols", img_resized.shape[1], img_resized.shape[0], num_col_classifier) @@ -2008,9 +1932,6 @@ class Eynollah: img_org = np.copy(img) img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - - if not self.dir_in: - self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) ratio_y=1.3 ratio_x=1 @@ -2037,11 +1958,8 @@ class Eynollah: prediction_regions_org=prediction_regions_org[:,:,0] prediction_regions_org[(prediction_regions_org[:,:]==1) & (mask_zeros_y[:,:]==1)]=0 - if not self.dir_in: - self.model_region_p2, _ = self.start_new_session_and_model(self.model_region_dir_p2) - img = resize_image(img_org, int(img_org.shape[0]), int(img_org.shape[1])) - + prediction_regions_org2 = self.do_prediction(True, img, self.model_region_p2, marginal_of_patch_percent=0.2) prediction_regions_org2=resize_image(prediction_regions_org2, img_height_h, img_width_h ) @@ -2066,15 +1984,11 @@ class Eynollah: if self.input_binary: prediction_bin = np.copy(img_org) else: - if not self.dir_in: - self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin = 255 * (prediction_bin[:,:,0]==0) prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - if not self.dir_in: - self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) + ratio_y=1 ratio_x=1 @@ -2107,17 +2021,10 @@ class Eynollah: except: if self.input_binary: prediction_bin = np.copy(img_org) - - if not self.dir_in: - self.model_bin, _ = self.start_new_session_and_model(self.model_dir_of_binarization) prediction_bin = self.do_prediction(True, img_org, self.model_bin, n_batch_inference=5) prediction_bin = resize_image(prediction_bin, img_height_h, img_width_h ) prediction_bin = 255 * (prediction_bin[:,:,0]==0) prediction_bin = np.repeat(prediction_bin[:, :, np.newaxis], 3, axis=2) - - if not self.dir_in: - self.model_region, _ = self.start_new_session_and_model(self.model_region_dir_p_ens) - else: prediction_bin = np.copy(img_org) ratio_y=1 @@ -2747,10 +2654,6 @@ class Eynollah: img_org = np.copy(img) img_height_h = img_org.shape[0] img_width_h = img_org.shape[1] - - if not self.dir_in: - self.model_table, _ = self.start_new_session_and_model(self.model_table_dir) - patches = False if self.light_version: prediction_table = self.do_prediction_new_concept(patches, img, self.model_table) @@ -3386,8 +3289,12 @@ class Eynollah: return (polygons_of_images, img_revised_tab, text_regions_p_1_n, textline_mask_tot_d, regions_without_separators_d, regions_fully, regions_without_separators, polygons_of_marginals, contours_tables) - - def our_load_model(self, model_file): + + @staticmethod + def our_load_model(model_file): + if model_file.endswith('.h5') and Path(model_file[:-3]).exists(): + # prefer SavedModel over HDF5 format if it exists + model_file = model_file[:-3] try: model = load_model(model_file, compile=False) except: @@ -3438,9 +3345,6 @@ class Eynollah: img_header_and_sep = resize_image(img_header_and_sep, height1, width1) img_poly = resize_image(img_poly, height3, width3) - if not self.dir_in: - self.model_reading_order, _ = self.start_new_session_and_model(self.model_reading_order_dir) - inference_bs = 3 input_1 = np.zeros((inference_bs, height1, width1, 3)) ordered = [list(range(len(co_text_all)))] From e17d34fafaccf3047024bf6d38aafe18967ef0df Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 22:12:24 +0200 Subject: [PATCH 375/412] factor run_single() out of run(), simplify kwargs --- src/eynollah/cli.py | 7 +- src/eynollah/eynollah.py | 148 +++++++++++++------------------------- src/eynollah/processor.py | 12 ++-- 3 files changed, 55 insertions(+), 112 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index 7dab4c7..fab0667 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -272,10 +272,7 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ eynollah = Eynollah( model, logger=getLogger('Eynollah'), - image_filename=image, - overwrite=overwrite, dir_out=out, - dir_in=dir_in, dir_of_cropped_images=save_images, extract_only_images=extract_only_images, dir_of_layout=save_layout, @@ -301,9 +298,9 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ skip_layout_and_reading_order=skip_layout_and_reading_order, ) if dir_in: - eynollah.run() + eynollah.run(dir_in=dir_in, overwrite=overwrite) else: - pcgts = eynollah.run() + pcgts = eynollah.run(image_filename=image) eynollah.writer.write_pagexml(pcgts) diff --git a/src/eynollah/eynollah.py b/src/eynollah/eynollah.py index 35f7898..18ae868 100644 --- a/src/eynollah/eynollah.py +++ b/src/eynollah/eynollah.py @@ -180,12 +180,7 @@ class Eynollah: def __init__( self, dir_models : str, - image_filename : Optional[str] = None, - image_pil : Optional[Image] = None, - image_filename_stem : Optional[str] = None, - overwrite : bool = False, dir_out : Optional[str] = None, - dir_in : Optional[str] = None, dir_of_cropped_images : Optional[str] = None, extract_only_images : bool =False, dir_of_layout : Optional[str] = None, @@ -209,24 +204,12 @@ class Eynollah: num_col_upper : Optional[int] = None, num_col_lower : Optional[int] = None, skip_layout_and_reading_order : bool = False, - override_dpi : Optional[int] = None, logger : Logger = None, - pcgts : Optional[OcrdPage] = None, ): if skip_layout_and_reading_order: textline_light = True self.light_version = light_version - if not dir_in: - if image_pil: - self._imgs = self._cache_images(image_pil=image_pil) - else: - self._imgs = self._cache_images(image_filename=image_filename) - if override_dpi: - self.dpi = override_dpi - self.image_filename = image_filename - self.overwrite = overwrite self.dir_out = dir_out - self.dir_in = dir_in self.dir_of_all = dir_of_all self.dir_save_page = dir_save_page self.reading_order_machine_based = reading_order_machine_based @@ -257,21 +240,6 @@ class Eynollah: self.num_col_lower = int(num_col_lower) else: self.num_col_lower = num_col_lower - if not dir_in: - self.plotter = None if not enable_plotting else EynollahPlotter( - dir_out=self.dir_out, - dir_of_all=dir_of_all, - dir_save_page=dir_save_page, - dir_of_deskewed=dir_of_deskewed, - dir_of_cropped_images=dir_of_cropped_images, - dir_of_layout=dir_of_layout, - image_filename_stem=Path(Path(image_filename).name).stem) - self.writer = EynollahXmlWriter( - dir_out=self.dir_out, - image_filename=self.image_filename, - curved_line=self.curved_line, - textline_light = self.textline_light, - pcgts=pcgts) self.logger = logger if logger else getLogger('eynollah') # for parallelization of CPU-intensive tasks: self.executor = ProcessPoolExecutor(max_workers=cpu_count(), timeout=1200) @@ -370,7 +338,7 @@ class Eynollah: if self.tables: self.model_table = self.our_load_model(self.model_table_dir) - def _cache_images(self, image_filename=None, image_pil=None): + def cache_images(self, image_filename=None, image_pil=None, dpi=None): ret = {} t_c0 = time.time() if image_filename: @@ -388,13 +356,14 @@ class Eynollah: ret['img_grayscale'] = cv2.cvtColor(ret['img'], cv2.COLOR_BGR2GRAY) for prefix in ('', '_grayscale'): ret[f'img{prefix}_uint8'] = ret[f'img{prefix}'].astype(np.uint8) - return ret + self._imgs = ret + if dpi is not None: + self.dpi = dpi def reset_file_name_dir(self, image_filename): t_c = time.time() - self._imgs = self._cache_images(image_filename=image_filename) - self.image_filename = image_filename - + self.cache_images(image_filename=image_filename) + self.plotter = None if not self.enable_plotting else EynollahPlotter( dir_out=self.dir_out, dir_of_all=self.dir_of_all, @@ -403,10 +372,10 @@ class Eynollah: dir_of_cropped_images=self.dir_of_cropped_images, dir_of_layout=self.dir_of_layout, image_filename_stem=Path(Path(image_filename).name).stem) - + self.writer = EynollahXmlWriter( dir_out=self.dir_out, - image_filename=self.image_filename, + image_filename=image_filename, curved_line=self.curved_line, textline_light = self.textline_light) @@ -4224,30 +4193,49 @@ class Eynollah: return (slopes_rem, all_found_textline_polygons_rem, boxes_text_rem, txt_con_org_rem, contours_only_text_parent_rem, index_by_text_par_con_rem_sort) - def run(self): + def run(self, image_filename : Optional[str] = None, dir_in : Optional[str] = None, overwrite : bool = False): """ Get image and scales, then extract the page of scanned image """ self.logger.debug("enter run") - t0_tot = time.time() - if not self.dir_in: - self.ls_imgs = [self.image_filename] + if dir_in: + self.ls_imgs = os.listdir(dir_in) + elif image_filename: + self.ls_imgs = [image_filename] + else: + raise ValueError("run requires either a single image filename or a directory") - for img_name in self.ls_imgs: - self.logger.info(img_name) + for img_filename in self.ls_imgs: + self.logger.info(img_filename) t0 = time.time() - if self.dir_in: - self.reset_file_name_dir(os.path.join(self.dir_in,img_name)) - #print("text region early -11 in %.1fs", time.time() - t0) - if os.path.exists(self.writer.output_filename): - if self.overwrite: - self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename) - else: - self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) - continue + self.reset_file_name_dir(os.path.join(dir_in or "", img_filename)) + #print("text region early -11 in %.1fs", time.time() - t0) + if os.path.exists(self.writer.output_filename): + if overwrite: + self.logger.warning("will overwrite existing output file '%s'", self.writer.output_filename) + else: + self.logger.warning("will skip input for existing output file '%s'", self.writer.output_filename) + continue + + pcgts = self.run_single() + self.logger.info("Job done in %.1fs", time.time() - t0) + #print("Job done in %.1fs" % (time.time() - t0)) + if dir_in: + self.writer.write_pagexml(pcgts) + else: + return pcgts + + if dir_in: + self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) + print("all Job done in %.1fs", time.time() - t0_tot) + + def run_single(self): + # conditional merely for indentation (= smaller diff) + if True: + t0 = time.time() img_res, is_image_enhanced, num_col_classifier, num_column_is_classified = self.run_enhancement(self.light_version) self.logger.info("Enhancing took %.1fs ", time.time() - t0) if self.extract_only_images: @@ -4260,12 +4248,7 @@ class Eynollah: cont_page, [], [], ocr_all_textlines) if self.plotter: self.plotter.write_images_into_directory(polygons_of_images, image_page) - - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + return pcgts if self.skip_layout_and_reading_order: _ ,_, _, textline_mask_tot_ea, img_bin_light = \ @@ -4307,11 +4290,7 @@ class Eynollah: all_found_textline_polygons, page_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + return pcgts #print("text region early -1 in %.1fs", time.time() - t0) t1 = time.time() @@ -4363,12 +4342,7 @@ class Eynollah: pcgts = self.writer.build_pagexml_no_full_layout( [], page_coord, [], [], [], [], [], [], [], [], [], [], cont_page, [], [], ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t1) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + return pcgts #print("text region early in %.1fs", time.time() - t0) t1 = time.time() @@ -4553,12 +4527,7 @@ class Eynollah: polygons_of_images, polygons_of_marginals, empty_marginals, empty_marginals, [], [], cont_page, polygons_lines_xml, contours_tables, []) - self.logger.info("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + return pcgts #print("text region early 3 in %.1fs", time.time() - t0) if self.light_version: @@ -4748,13 +4717,7 @@ class Eynollah: polygons_of_images, contours_tables, polygons_of_drop_capitals, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_h, slopes_marginals, cont_page, polygons_lines_xml, ocr_all_textlines) - self.logger.info("Job done in %.1fs", time.time() - t0) - #print("Job done in %.1fs", time.time() - t0) - if self.dir_in: - self.writer.write_pagexml(pcgts) - continue - else: - return pcgts + return pcgts else: contours_only_text_parent_h = None @@ -4834,22 +4797,9 @@ class Eynollah: all_found_textline_polygons, all_box_coord, polygons_of_images, polygons_of_marginals, all_found_textline_polygons_marginals, all_box_coord_marginals, slopes, slopes_marginals, cont_page, polygons_lines_xml, contours_tables, ocr_all_textlines) - #print("Job done in %.1fs" % (time.time() - t0)) - self.logger.info("Job done in %.1fs", time.time() - t0) - if not self.dir_in: - return pcgts - #print("text region early 7 in %.1fs", time.time() - t0) + return pcgts + - if self.dir_in: - self.writer.write_pagexml(pcgts) - self.logger.info("Job done in %.1fs", time.time() - t0) - #print("Job done in %.1fs" % (time.time() - t0)) - - if self.dir_in: - self.logger.info("All jobs done in %.1fs", time.time() - t0_tot) - print("all Job done in %.1fs", time.time() - t0_tot) - - class Eynollah_ocr: def __init__( self, diff --git a/src/eynollah/processor.py b/src/eynollah/processor.py index ed409f4..8f99489 100644 --- a/src/eynollah/processor.py +++ b/src/eynollah/processor.py @@ -30,11 +30,7 @@ class EynollahProcessor(Processor): allow_scaling=self.parameter['allow_scaling'], headers_off=self.parameter['headers_off'], tables=self.parameter['tables'], - override_dpi=self.parameter['dpi'], - # trick Eynollah to do init independent of an image - dir_in="." ) - self.eynollah.dir_in = None self.eynollah.plotter = None def shutdown(self): @@ -81,9 +77,9 @@ class EynollahProcessor(Processor): image_filename = "dummy" # will be replaced by ocrd.Processor.process_page_file result.images.append(OcrdPageResultImage(page_image, '.IMG', page)) # mark as new original # FIXME: mask out already existing regions (incremental segmentation) - self.eynollah.image_filename = image_filename - self.eynollah._imgs = self.eynollah._cache_images( - image_pil=page_image + self.eynollah.cache_images( + image_pil=page_image, + dpi=self.parameter['dpi'], ) self.eynollah.writer = EynollahXmlWriter( dir_out=None, @@ -91,5 +87,5 @@ class EynollahProcessor(Processor): curved_line=self.eynollah.curved_line, textline_light=self.eynollah.textline_light, pcgts=pcgts) - self.eynollah.run() + self.eynollah.run_single() return result From 79003a083cfe5e23ded73af683751797c9d10fe8 Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 22:43:01 +0200 Subject: [PATCH 376/412] CLI: ValueError instead of print+exit --- src/eynollah/cli.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/eynollah/cli.py b/src/eynollah/cli.py index fab0667..852f06b 100644 --- a/src/eynollah/cli.py +++ b/src/eynollah/cli.py @@ -256,19 +256,33 @@ def layout(image, out, overwrite, dir_in, model, save_images, save_layout, save_ if log_level: getLogger('eynollah').setLevel(getLevelName(log_level)) if not enable_plotting and (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): - print("Error: You used one of -sl, -sd, -sa, -sp, -si or -ae but did not enable plotting with -ep") - sys.exit(1) + raise ValueError("Plotting with -sl, -sd, -sa, -sp, -si or -ae also requires -ep") elif enable_plotting and not (save_layout or save_deskewed or save_all or save_page or save_images or allow_enhancement): - print("Error: You used -ep to enable plotting but set none of -sl, -sd, -sa, -sp, -si or -ae") - sys.exit(1) + raise ValueError("Plotting with -ep also requires -sl, -sd, -sa, -sp, -si or -ae") if textline_light and not light_version: - print('Error: You used -tll to enable light textline detection but -light is not enabled') - sys.exit(1) + raise ValueError("Light textline detection with -tll also requires -light") if light_version and not textline_light: - print('Error: You used -light without -tll. Light version need light textline to be enabled.') - if extract_only_images and (allow_enhancement or allow_scaling or light_version or curved_line or textline_light or full_layout or tables or right2left or headers_off) : - print('Error: You used -eoi which can not be enabled alongside light_version -light or allow_scaling -as or allow_enhancement -ae or curved_line -cl or textline_light -tll or full_layout -fl or tables -tab or right2left -r2l or headers_off -ho') - sys.exit(1) + raise ValueError("Light version with -light also requires light textline detection -tll") + if extract_only_images and allow_enhancement: + raise ValueError("Image extraction with -eoi can not be enabled alongside allow_enhancement -ae") + if extract_only_images and allow_scaling: + raise ValueError("Image extraction with -eoi can not be enabled alongside allow_scaling -as") + if extract_only_images and light_version: + raise ValueError("Image extraction with -eoi can not be enabled alongside light_version -light") + if extract_only_images and curved_line: + raise ValueError("Image extraction with -eoi can not be enabled alongside curved_line -cl") + if extract_only_images and textline_light: + raise ValueError("Image extraction with -eoi can not be enabled alongside textline_light -tll") + if extract_only_images and full_layout: + raise ValueError("Image extraction with -eoi can not be enabled alongside full_layout -fl") + if extract_only_images and tables: + raise ValueError("Image extraction with -eoi can not be enabled alongside tables -tab") + if extract_only_images and right2left: + raise ValueError("Image extraction with -eoi can not be enabled alongside right2left -r2l") + if extract_only_images and headers_off: + raise ValueError("Image extraction with -eoi can not be enabled alongside headers_off -ho") + if image is None and dir_in is None: + raise ValueError("Either a single image -i or a dir_in -di is required") eynollah = Eynollah( model, logger=getLogger('Eynollah'), From c7dc95285193bc34fa03ded6a397c99c6d45303a Mon Sep 17 00:00:00 2001 From: Robert Sachunsky Date: Tue, 1 Apr 2025 22:43:30 +0200 Subject: [PATCH 377/412] smoke-test: also test dir-in mode and overwrite --- Makefile | 5 +++++ .../euler_rechenkunst01_1738_0025.tif | Bin 0 -> 7223024 bytes 2 files changed, 5 insertions(+) create mode 100644 tests/resources/euler_rechenkunst01_1738_0025.tif diff --git a/Makefile b/Makefile index cbb0659..27eb872 100644 --- a/Makefile +++ b/Makefile @@ -77,9 +77,14 @@ deps-test: models_eynollah smoke-test: TMPDIR != mktemp -d smoke-test: tests/resources/kant_aufklaerung_1784_0020.tif + # layout analysis: eynollah layout -i $< -o $(TMPDIR) -m $(CURDIR)/models_eynollah fgrep -q http://schema.primaresearch.org/PAGE/gts/pagecontent/2019-07-15 $(TMPDIR)/$(basename $(4HMwkNuhSOvy25@B7K?#r zO=*OfbQ-f>Lu$}yj9Nt@D=cI&Vo@s#MN~7k{tO0#PN!q(x9XxbmVrO5pw^ViJK_Ou z5{t#c{i0qkx?a!SwDgB=GMU;f%O&!^(gV^1zex|s&-34;_vP5q1JVQ11JVQ11JVPp z(gPlcRS47!CMTnzd^%CerK`nUCJ_z#Fxx?AD`DtWM7$D^bvkH=$_R=mRp-tZDo6(= z0(tlocJ*5pG%fzr`cioZJ-|J{Ey0CD%08bjkw^rCL5IVE9V03CkErktI!9iz^uTND z0riu7h4R`?SWZlOKzcxWKzcxWKzcxW;3Yj^)@dwyjn$xWnDt(VCG2x0BY{jjTu8^$ zu~5YC4tg9&$m^tt)MnID-=x(L1w>HDVkE>YgbwLVts@BN1z(~lq7Eo6X!<3mEvvoV z9^gkBV7XSS#bPm!#{(vd>*ZGA@9n;Jd3DkQZ=45Ke8Q{QH||2@grx_h2c!q22c!q2 z2c!p9ct9oJcKC&zJ$AFpYDC;Nliy{F20VhJVQ0U~?seEbc2ZE<9Siy>a@*|=i^W6) za2cFY%rAj9DlfxCDi*Dnq|Cnc9$?7J!MdvbWohFUlps^8XCOX^_pWW!S89Y|K zD5U~5v4q^#pvw|)nn@#Gdotw7Cqkukbf8i0spe6lJ_lW~-WoBlnxf#Hb}kmYEbQ%M zw5q8zyv-f}koju|VRUufzPNI6tB}6Uj*!vUGV z9d$7g z!1C%pv0oKxmJvgFE8+p}g!Z3J1SUWC-k^xe(rMBI(gUxL2UI{_p}amblrxbYkRFg8 zkRFg8kRFg8_--C>>UDO`xNe1&MQ~Z94xJXEREW!<7ZP-PEv${zXlw?P!(=gOlmos< zT^5_y?hLp*ey1D7ZMA!B4xhu7i(5-+XEo=k<-JIw6zJ=U4s|C78u5D3p9&kp9!(;s zr&`pftPHpFSm#xf5f+<FWg|K* zqSfmVok5Q**+uEElYY0CLAH_}kRFg8kRFg8kRFg8kRFg8kRFg8kREu=JYdym6wwkX z*4gxW#BMMkDlDs{EWjBA7QAfL8|_B3QKLLb3SKsA^<<%DNEwcXrV%Vqa57}bM9riT zuLgmUiz!F59E6ua=|a-kRq%n&aC9}7u4IyhWRlg}=}3g_&+$MY;B>mo<~Byw5C<2t zwHqvgmqn^Val4||nT-ak*<`nv9mw~_#cMJ3Yxere=|~Sq4@eJ44@eJ44@eJ44@eJ4 z4@eJ44}3Qdz@Y?}L1v=N4xJ(>LU-!5E`v@LQR=o@%`Au|1gzDvkejDap^KEP(+o0O z93~Sw6%e;Y6LM=}er?dDoD#;CXOwKrQb^hfIz!3?ox3Zc(Z0;&P+@!^H`bpY?aK{! zD>B%~^;DA8OgJC&`m7p{S?4q;Q*CEwfvq}?R)}7!6QbhmiY(_vAzF&%pY(wAfb@X$ zfb@X$fb@X$fb@X$fb@X$fb_uH9&nmWE|bY^GI>l!w^0ux1Ispz0?z1Igs67A(FhCk zoMo|X8B*rVYZIc=QRKDT{Z1#Rf`QC7g8_k&;bnr$QJ=wW;lOQUI$|TZ91rLSF2l>D zEXFQpTrAZFoSWrvZ!I?1$c*+D#`?;`-G%;Iw!4%hZ4}alcsSwrhdmyj-A>S%IJnBi z?Fv9_(CZOaTesWM^?EIWnZ2>%Pi(kb+e5NMdO&(WdO&(WdO&(WdO&(WdO&(WdO&*M zUG#w4q$O_awdjSQV{!4sD7}VM6p6sDt+mbBMgY3C%b2J%$gISswTfVLn+*YnHR!bY z?G|*GNw1`4Weqm#vDu{TcW4lT%RSXF>$D+dmShuXMkfHBj+g-FWQe`YmTJ!3EC;9t zK{raFdNBw-_jje5rFf$l>n%rk=S&?Ojv(2n=a6DNX zKNFMI=@rovBX6@mqicA8QAhGF-bM17r3a)3qz9x2qz9x2qz9x2qz9x2qzB$U57>1Y zK$u0&knhsMY*uCKpYpQLY5=9xh0xRigb}tp!^<|-U>gk%gC34{8g(9v5k^JR)Tgj80cD?^M>Z??3>SmXtiw9UUqT^k%y2@vcg^Rt}LO`HUOMBpvaHIp9@-%S4Xe%Isg}Ol)GxxwtJC zXScGAs4=Uu;bqRmCKbHQ7G|g!Ixb~w)w~mA29xtiGvM4+bi>O8ond51xu+WH>xy(0 zykzUu2y|R61rhX~Mgn#o87%boq=yIc6QdP?nrx$%&!&>GaLDI#c)fO)%WCJqY);NL z=wAfOp!7?VvZ(pi-5~OUqz9x2qz9x2qz9x2qz9x2qz9x2qzB$n4{To?+qy79x^LU; z;XR9cx6aIt)_W@PxZf1>86zHjz`@337FR3cG%A5(r(WYVE8;RJ;^PEtho1An0yZr| zDaN2(hoG}z84xC#j6zz;x(PIkUCiQH%!HL;XCcbM?5G}O&cqCfkR}z;5o<=s0?~kT zchx)67aQyjS8}#&Twh3;Qc-<8q{${sz4g%8V0v^QH8q-FoU2cc2EI_(;VO=B`>^z4J?c~op_ZYe94bBiq7oh=4E zkEgO4d2+L|Y1+(VtMu%ICRDAQB*rpu{!rf0yGmZR^uW9A0p9z36sSH>KNeJrRHO%_ z2VR{A#5+#?xA@f!Bzu+~kRFg8c(DiCzYM4~pKDZ0rSckj;M}oYrw(m9wtv&{1Dnqr z*@2)O+_iwf&Rgb(XGZG%U4=>}k&OmZAurMAfXy7V8+;a?Hnqggpt0X-j=D{V&tmjj zjWLfo?zMy*R`x8z!Ysg!`OTb$&4y+0xtQ_+&g@(!CDu&zne)Eb;0#KO_1Hv~VPwFW zKy$6&64VSYSM$W0osF^^K_Lq%M+2HzP*cpM<=^z{Ajg*;kRDj_03S-~pLEKb=z(*`x1Bw<^~}*Nr05q< z?Ywe!&((8#&!5-Yh&aTvqF8awVb|*>YCK_?he-`I;$!_;@EKSZPhn;yHVWIB z(-9uc%rRfiOwfmri!wxe0hEo?+rO; z=>h41@8$tM1Plg4`{%%S>qoYd9*`dR4j$m2UEY2CDJ#9?zrWwXgR+VAfb_uk=K%o% zd@JCOPb9M6U*EE)e;{%WW`ykNBb!ei+DH*{>hLDALe9Lm^~lbJ z9rGhI1C@bFs*z3AGfEn9*dq>e$Yuyw^dXxz=F+8nCa4)+W*IhIJULX@wRv=Uqy#1p zHWRaB6(E|e&6J`LfF{b!UgoGz*CJ(GB51^zPuSSDteo@}H?pIdHP}I##B{969_fpe zGN#_HARH}9`|6>AW(0x;uCX*LK1g|Xyg1sQ1f}6<7JK{LnpoHp4VhDM2T+`h+5H|Z zI^YZ)5A^1sX_T?y%Jf77>F+L0jC9XU4bDyuOpWzUj`oZX4-WVD5B7F-m2-(m#N%+- zc@nhQY~gLN{GzGOa8>ElXa0on9I_ z7wLg_&;xuT@s)spnuw`MUVanrfs=Veylz-P#SHd`^(XK z##f3t@?k3?o+JenPYoC0<tza{E5 zq{B9zCxt-M%4uM63r%2QV#j^;Kq{i?%Dak5OFFE}$4$+Wx4$b?FSxT&Ltia8+#Q=7 z%*>4yXGaU;{i)t+pq6uj+wq_=;?t!fmRcc@iP=I*heWQO#k9Me^>!75BYnAbla1N& zuA!dHaBp^ItTsQ@Bt;fydp9o(ZkX$%0$J)%G&fQ2ZzSo^{k&z@{KVZx(r@BDBIhhU zAU)8T2P7|d<}X=Wdfh41U&{lMmwzqyy&jG4Ts?%`y8PbNb9>I8 zAhNs-Mixc^%L1P%y>y!B^VUnpcAj}}^O2qFAmzh5<{)M8`Qp(Xrw?oboUu^!-qz`z z3&Y#y26oI3v5I^Bbc3)mYq^PKvkaT9$_PvESI&{`S=ax328JesRy`vpX)F+KQlD zJ--X(^wA9`53fIWV)Knld+*;oeDB5~D&IMO;L7nGr}nKsx^wo}uDO$Y7ld3sw*A84 zEu?oZym#;7!5gRdTsyhz_PPC!@0_}M={>Qo`@+c``?gQAg?XemO;{O5cJpLslag{Q z7@|C^*=5Z}{DpWR?z0CSdcw_#fHf6@myM7zQD!9`oiK3%7$;^Ug}AkxaugG`OxTzu za-DMIqUMNO6LM-|UJXVnP)aJKyB02HoPL)k1XGs-&1$Heb7QO)yrrxw9Wy7whCXuDNNUcW$z}b;AG_N*k5Ha9?U-IFE6|y6(lr#mULZ zMxy~WI~)#z%Wv$hAtx?9AU*IMJ;1+=y!YFGC=*}czhif@ne>44KqnrMyxfVmWL@b2 z>48`70X~$}zgIq(9N-mr;L@4x&@spi4&S|g5HyCAPaRqO!N-^1zkd=y#)4eCup4kb zesKQD_2UpREDSXR#o#kpk&?Z2cHiS`#~xleLh1QKn=c*RcK^mPm6z{cJ3^L?(wrxD z>GZDiCw3g%y?Oog=tyt1x0Wtvg7E;4el{ip?o8MdcA4EKjmNABI(1>U0a#{hGl6NA zc5|GUIN*y@yn+r*GN4NZ^)ND$4jIsaWuH}(3L0uzccb8|q@AUty({PGt%SNtet0?J zH|CO#@uB?aK(>qRMs$2AH_#m)>`4I5Q=>&J z+cyot$S6QGq>LQ}0hXs)e#g$u|K{=@47)N1J6Z(=NjLnt9tk5di}ya_9@@Jc9<2{Y+Js4 zvWxk%_pd&>efs95LzmC&J$r2Xsl!_?o!)cr`f)6G zt{(m1{&{qa7f$Yact8z-)u-FN2bwv&f8W4Q}T-#T^U;z1f7+A~kl=_6aIeEZ4~ z(oO67_imdyad6YV?bCDPU5!#G9xxDbcA1p`G%L3qMkN^Sv1)=Y4dLcu%GFf}z|k?k z9+YmK55{w;6rGgPY|KU}op9!MDeZ2S!~OLH$8?M5Sg|0S4bcENtHEJ#!qyx+hM*{q zwTiJ0T-kg*&`8p-obfQ$Sbu&4`$SSpL!Nog^SgJ)$t#u~czZm+7xESV z>T#Nf)9EA?7fGr7y8iaO-ttPM2c!q22c!q22c!qyJrA%+8CXUDXTi?kGnkAnHY{@j z7`lMyQ-?P`xqBAug?X=EV9WDeF~pLl%d8lInh7xzZU&jD2@b=^JlUB*Gf>PnYEB+Q5dmew%>bYjM;_z|r?_93}J&4ui%7kqH*V z&cu}AUoe>vGb!t_Ng-T}RDg@AK$hS#3avzQbeI}m7E7{;K7-5%;byoPSO$R<=1i9WLmn$^w_G-2Oj%99A%Qej6X>I^t_EQKTn4H^T&Vm&snj6lutfGH6) z5lZISRS0opYQopRGIR_kQ^aCyU>QLHzzI(iWrmIsR%H`t29Q~yO>e+5tFp0^WdS*s z{%U%(yGU?(uqzEdD{$SPAM8$-3&Bz@oJj^EL65_3*XvlIZItV=-?Dd;yqtH<1H_J3 zgpWD>(rUH({eE>xw(1kPFk5wnRHO%_2c!q22c!q22c!qy6c1d#z|w0rDIeq{F3z%k z|K6F89-ja7$)%4UU3hXA{yi)7hqq236cO-+c?BAawb@WJp=9=>zo9l-)PAAfHnfHkb@bQxjOG zEg@#&%Y?I8&P~sB1$q;|CXfv{PYss|Ta#r(ESZp_mI?Ki<2|Jq91Y9!1ZN&ZRV{_9 z#aJ;HO+*7uhfVHZepB8ca-PxyFYtii<@UfaP;9YSd_Lc5UIwLK(3LEb9*`c89*`c8 z9*`c89*`b*4LxxC%Ap$<55UXdFn}zO`IE;NAKW|+L4Ww*+?O9;C(F^+2oMeb0>S|D zr%$eY^7t|enEd$%*FJo30a6xGX7CpHg`GuB>>x7{XV?}NrbsX~7QxqGH4sfdfHrMm zZDP|9G^@CYHIrfojfGBp8C~Er?92pcOWYYFMOaYSWei;J*f=rP-(4?e${9aTaE5bz zb}iJ*dThX%bHiAAO-z|MGGS%09vf0d!N_WD^pKm=!;Cb6g)3PvowP6Zj{3}yGIoF( zJN+j^c8^ID_nPxjXEE-k2%zR+&1_+Ab|vbSWIhuP`kY3Cj{VC7m*xKD*YF)7rzAbl zkq5*MWdY8t!R9=C7Grbdml{o0XP!~p~G7Zsb0`O9qQr1z!!bF?_Y5~6(sYVa5t$=9i zfW|aoAV3(DrZ;F8ANX0arjC^Mi<9fqI+Yqj%0?k;6DU0|rI&~VQKx@smlNH^ZA;ywy zvHck|CJRB6Ws5YW;&?G4(S)Jl<*-v9_ZpLaHbmQ5zs>pB<*b+W*p))Gkc~t`UaQ4K zd|9tyFSFxyxCi7Mqz9x2zOx6wVbECo!ONWV#TI6h$)t8jkm>z!c(G3V4pvF!s;1?EVVuG(xs6#1(0?72w zD6GWha4?Kwwj37*EDNBfxj<|b4%{Z_j6x5m4s-tY{I zlFgQ9LeFG5IvZrJ<-IJ!=14KD)dEG>< zSq|Ft8g?!dWfrTi*|RJ*L<7H2Gjz<+UZf(pOc5bwcv~EVzn@h%*F=1?)u#5EEY} z1`SOUO-3k!TL}#Vt<)q1Y~fr|>QD-61H51_buf}eghDCxS#eFQ8P>)w)?=fHP&En= zOsqc*sJeGX^|d~9*`c8 z9*`c89*`c89*`c89*`b*eLV2-!)uRkgURPUxOe%Z2gH;wKDcp~>}OAIqTjuG>h|Rm zSg7;p*7^I_&xr6btP1ubD1fsF_(IB*il{LS5-kY}jwofliHQD}5h?#&g;0!f8 zP0HeJaa=ZAl{wvu1F_k>T#S3mNpB_PqmIk0X;vb|w7XW0)JpM2E!$l$=W`ja+edtv zM=-xWcY>UW^nmn$^nmn$^nmn$^nmn$^nmn$^nmn0y9dBzxEKMJKY4UrATzxD)yMb9 z!pOifrBr+W?j^EdDU=G9wqmtMfLsI)#mKT}Ld=jdEDX|;73@rw=(AvI7?vzWR0Dwl zWcmS|X)cJE#nn_s7a?O>p)(w#P!rImrWk@EFqxrQ2aZmkfHPgukdz=b{h$zWW~cM9 z0~^@COhY2eCk}29TufM*`11Z8Q#>;Zh$h5Ld>Nqr?ETAE&+Vm%h%>F|igD=&?q*T< zhS^>o{X8|?9Bft^r39Eve3_No4x>WHPQ51TRu21$y7grJ4r3|fspNdcoIe?L1$|bp z%W5|9@MYzEY`K5A{Z^E^^nmn$^nmn$^nmn$^nmn$^nmn$^uTN70nnJ(u^?gqnXobu zXR=>@bXSC)DHWk-co}vEfgx6b$s!O8%!<>!KwXGcU^2XnK&)SUcwJZ^W$KIQFo9r7 zfm~1&3P!+QO6i#%0BW)zDp6?&7r{=JQAB*1Zn4l6Jy7RIKe$hdPM{urmv`p=J)vX2Y`AtO?i@v1v4%3C8;Epj#95>xnO?Vs4+? zsi0xXjvYSQP-iu8c=fb@X$fb@X$fb@X$fb@X$fb_uY=7GnzE`D(L%9God?q56m z=;nn_AKgSSA`fqz$MWI5tDil-^~u8<=(jGO0Fc3Cpjd2GhDPCBa213kl+1c;2p1#O z5Q?B_6zG-uf_$lrkzh3S=?ZoxLXC({L_>7QSFEX~XP^~Q7GY=@nf|dLv;}5=`t>7W zM5hjIvC}g{Frw%X;KlemKl*_1F_;V#Kf28_Z_XJz$R=eJc$t{8;AH~MM4u6ijKw_Z z1X?Ff4KhQ@7?~RY4%A~O6*LV>-??<++L=S=kM7z$JILZ~o@2%Hqz0O)fLCWTX?PB+ zSc)yzW4~^11UVn+0qFth0qFth0qFth0qFth0qFthffXJAhhgFO?_9onS+>pcY^C(j&htn2+&FjW=$;KskAVBh!&~lNKYr!np?$lyj13Q0iiL0} zNPJnZGaK~syss7aqs&SVNDoL4NDoL4NDoL4NDoL4NDoL4ye=Mia`);NAKphkxPSe_ z2RFX__~9pyZ{NCn8hLz&J;-;+zJKo;I?C6dKE8M3%+1TkpWM6j@#E|04{n|N@ZnX` zAAj}!Z+-L84?lnS{{71jZ=e6@(KRYOzI&0BB9WztnqPi$59Q9aQy4#aasvzU*$20N z@bO(Nw51jOBPfsVToCfrr}wFL_xfoXUOj*K{>`(rr2_ixt0zS(jLZ$o<+BH|&?gpA z=jeegG^Ytd(X|UlE}eOg6#erLZ~y4aCtS;kL)&Tl^=A(li$NKP3M{t;42{qQl+%_?s8Q}@>soT_cN z>y1W@mUy#H<91o~I*nGNF&Z=mz1C#Zn~f%uf$h#ly%vK(QH%zI$!Ij2P?W06qGZ_> z&HwZ|DyXGIv|p(ZFn_h8(`Y)8S9-CsbLoL!(*x=o_7y3==JbAL^Ny$8(Yu$*%O7Yj zt901EUZd+sUUnLtPJ6ljuXOmT3a@zn9s2Xte)xZfe_WOw`?4=wr$emOzc`X)b-Gsl zwd^mRkCyKWMy<|-==5ep`A5Ga>Ew&2jxQw|Mr1g~vLGmAF`_K#t*lZ_h~A{p(S$E2 z&x+BaF_<;&rBp)-ej`76r;E(qeW-7sru9F z;&RlsD=uI4iMZ<3=?dr2T+~_7&x$N{#Dtga;uP7{>2u$w%6YT&u-x{glr`>8tHs$q z1e*jRIc+0f@c)7@u@+WSM)o*r-RWFR(KHOPCH=35d zX^R(}P7v+|q`mWJEN$;oF_wCht{9Y3is+|Z7|3AIm`oZAcN5NT-HhUn6V)(^B9W!C zzyY$Nvf3M2`ca19!WpGC9N87qXwOoI4%Hso6$Wk33Z4FW&DcO{W6l~y@S@NiTmlnl z8A;7r#ZFC8nQCg0XeF{#LqvUQx)_xC^M+$qtqT#C!#u4v4R1$0)8_Bi2V3i{tGM75#_PFecU_?7pYxYJgrk8bn0WQCN0;*TEQ z1dz$zy>{;Q6+*_B&;jau*U#U-aRL1YA1N#e)qA=nixYmcpJN zJh*}#=A{S%1(3;NBnwkh4IMy6rxis&H$r`QmQs-wO++Uc8IGP2!N`EHu%J*%Lt3FT z0R#|cN=%RD*DoGDesCKW>OkwHG`w(X|CMuxn2#7yMR=(H+aEl}$mke}YlFi?wBcev zn2<49NEtLHATHwC=q%DExJ(g7s=>%?XU0MiDbT%p%f$X2GxUZIETe$WbbI;o=~JhU zPEAdw)5&-&=<~YUZ0d;6S=>+DIVOwQV&(rJHTTl9_nC52X?1!X_ElfEzGr-1 zz7NIS_zKe1H~0%Juljmr|I!1$jt8Fk_h9+${XH6gow>bC+m5HY{I$02U#J(pfjjj{ zhy8a1%T(_8dOMu|%lQ1+I()_GKsug(hyHx!pZ{xoKVNg)4*kDoKWmMss~K zvadDnTK2X2S;GzjxBuyv|1drUlGfKmvJ@H&)~A@f49*z!ExRbvVppkoE&dW%w_K`4 z5yE%n7l`_O8a!eyX1*Uk|IPlc5b^5=|t^znU0r6R7dB9K=klVOL%p}D=6&GOP}6{;tmvds2~?Ctv{9Z zw9EGniXy8&?nDJ)M}P6(vz>GAJpc1w^tto}n~NiswLG*6p?cz06t%csHLj&7oT%lN z6`VkGK?+w=;{3ju!B3)DgS+zV{)Y`xQ=00L_WlVM^=%OT3`&ooSg*eEyB zx!L1!heAQRqGz+&C za%q3iYcSy&joD&!xt)mOQgNBBOXLb#fkEZwQH1!P(M+OT8ozItn0+e{C~mhF7Pmfx z@Wrd;Wdg@44uAaU7Ss$TBM)xEhF5@PbSfYeLDS!S@kqcdWD0K*&xL#irILc5@GO*y zQ6MJ~VThO_0mTp~;bua*lwyZWMOIL@;APsvwg_2--iRPW=yW2um_Dh`G)U2@Nw)~2 zQ&~&^h02uD3SK_;-d60?0n!l~o;kLg5Hod{3|WLybby+%=$vU_Asnm%GCIrAA!2A3 z1=YyGqNO0Mx$`|#K6f2}WSYuP(>(OUhyt@c;z zpH6%4wEnMmFa7G#eszr%Gg#yM?f3Qu?y&z3qjAswzSOZ3R`{eX+orx`ubsW`Ub54t z)jIU&ny=$;s-I3B+UY#k>VK_&r`D>!mi_yh&l>%6_pDK}hF!#To^8X=^{TXjKy96_ z{2HR<)i3<2q7v#Fqdw{x&)c#i}YKN;8db1c52va&B4o?P-T9<9 zm++t@!}fU48u6KsC{LU9n?oLh->D6Ijp?Yfkn&NpnD$rm;c_;ZiMhOXO~A#ob@bG6 zTbCla1bH1cjmx3&y7d8{Dd;zo`aA}*xlACN_GePQRKk;pxndDVG;EKAY>dLtOdu6; zFf?K6z+S);L~{6#G_x|vYBeZ7IVpeqGKS8tNciTOwA#xA#U!M~PUo^ijvrJO;bP;wI9gCmgO9O;FUCsQs)^M2Dh@+SY0^ zp}Xw%pwHv?xNK&ABY{|T*wINkRo_0k6mB=5ekLzsDK$xkG+WByG<*rJxn9Gr^SBnF^^-MCHiicuhYI@SUWN%lJsTR{jsNG?Y-S4mw;6`{gc^!H6dBw#? zh4_!MbXNHYQa%o!x#OQ=vf$^X1r^Jjvif7Xvv&X+mWd;S$)CM{_wx_#iD)ut%sOrC z05TlS&SjQrV*#ZFNJ6`?Ehq|piZ#{1G6Kdzoft`lf`yQ@h$ch06cJDckO||$vjTxJ z!pra}^hzDF#!k{C>C4{Tr)%L7+H@EFN4y+ zINdUUVKy>jUE$=w?lM>cI3 zzkKn?;e*@GpFMd0?!^yT_kQ!j-d!8JDp{A^;PpAwtr^@od=8o|IxBk#S~nA){9?bx z(|;Y-`1iNd@0qgx>*|3{f1OzUiSc4fr~SX_^e5&Je(!kZ&u(M< z)hieY?Q|p^_W$&!=9hcnI-5*KGjCPy`1N|ZX)j-9r@s#no9a-OzQ(Mq1zI^)PySW{T zMa}lrmaBCuA|zn#57x%Af_$!_BlJ)aN63|sZ4|A zh&qkSsPULJPNJn;2u^6(DMCT0#;n{HMNk-*o*BqveZF`t8oy18xJ^sF(JlS^tja+2 zPZmAuF((5AA05e%Ar&?v>4=GxY&@Wg`L)8hIt@6qQLiZ#vZuq2WYCrfSkbdlS25|U z7oxq@6lpHzj`_?KWg^aM&R5BLOKEo@>Et+|Ow^hRn~{iDn+%z-6jQEp#zP%a>d;Rm z6Qq-#O0ut(?(fQw((Q0}VQQ#r-FVN;NMp3OG}2QfyMC&FYPdW;P#Ee?_ts*~a=1|n z386!JrvGfrmQOh7oJYs@RHN&L%X34e>HhpgPiCx{8mT9SyW$A?P%YM540h-JjjXqp za+PAXT*yQPil~6Snez=+qgX^b-kqN6%h3;|{pB!ik!s#q$vH|HTP|VB#0wOR2E zc1MSM<3fgdVq}qlW&~j-^g|EyNs5kTcCaX9{a9`DboYjdI@3nqzOH}Y#-Y6%2KOuu zAlqh}>qkq|ed&qr#7I|Us2Uosg=hLRTc+!~7W%f&b#I)gzPDv`|E6JDiH0;;HlaRdtAuU^@WPnjC(H|$qKh~$e2QQZS?Y6|mZ%R41HxiO_iumu zF$!1;rD6nisR_x7=A?8>k%<2ilK$xrKg38KMkEAG6F8a*2(3gvl!~A*yo_B8L=pCj zrw_otG^7(Oqy(M8XNa4cv;xLaAZUWlM4aiIPM8%)3)u2VDae;a*T65p%h_Kf z=d(r4tSf5$l-%+6vDHK%Ew8f&Ui^(?b!n&9vbu-2+wzM2^V;!Ohu7Qj{5#~?SKa?I zNG|{2{Ht!}j_1=MGI#v>{HrGX>;+0*UiHu8>VGi52TME8w(I{iqrdzpEAF1>#$ej( ztd4}f*uqy?vsP;}7@TIa%VHsA^P`<0kzS7xU0BemMrF2|vB3MOQ zy4Y?uy6sl4)8V$;9fT|`7LV16cx^VH-7duMaJbB7(vZ^`a5$(#mI@)43!~3YO~i@> zoQ{y20G21}^Th-HWGIjdBf(@S6b}ZYet%rqYw2g7X3*_MLT*>Y>x%kFJ#5M3irA-_ zi-&9ZL@6E5#A1nHAQ23*LsJ#97|Eui(Of*5iAF@v7_+fZt3FLuN-`X7)sGjF@nR~0 z6q1|=8%u@#>4-lQrELHu8S=;23hMPneeQ^t3vmZsE=8EC$AhGTzPQgD@pu%Y(;0R- zVqQnW??{Jy6eaxL7;S~%1e&T=25Ve(*dv-Wwb9h(wl{6IX)L$1!lVn0U#QfMd_|YfL!c@ z&D206i9)x6jwc6ev!l(qv2Icr89){m^ofBg$UNR(0hu?g>tCGdSvOG!oF|7$$n;2M zcB}?I(>X)b@_s?e%weoQ4~+u3lf79umvm;J0E#Y*R>_Vwk`Su^WVlsiH%>MnR#;VF z?3S6H-HXG>o(&`WHjN$N)T7Pg=)2YrA=uYVmXX=<;`m^Cq%T3bex|m4V?U`X3)59( zeyV~jj(0HuF%8DrHrEFfZ=LPkyK!{q!XOV))q#JQ1gy;Js>a&Suhz`rW8homq`U*lVXRL#~LvdDg(|SHoQDPT-Y#PQFzOPjtSEP!DS%&XFvLcwy-n( zbCxxn3;B|=;u?iVM1A|kJrOk~3oir9U^3BU5mN@Ji9Qo|7SU(y05Z$7MYx%h!N6xK zusj=x{`t?||N6_DcWxcNcz);E)7uXoSa<%+uAhAS=*q?SE}Y$a_SBy9XZBsbaPZ=} z11FAde{b(5&Ih}8?cB|q7q4GGfBg7?$;m;V&&EGt9*>2ef5pFL{P2rTN_Ne28-8`5 z=jFcWcJ}kkcc|U|ulGyn-`91?exwIpi3d95saMTw?^^$z^6Bg5pAP5GF}tr=p5^nG z{TbqIYb90%uTEd$U#yN@K`hupRMeoc8JAWguO4?Ti-=>A5}Fi|L;{yYONE_iqcDn? zCILsMO=Gtx`!vPoO%$>i{T@XEUQN)aMM8dEIG{(8UTr2|$cN1Yi7Le1;&$R_g@^?y zMy+HKlx)yQ&A3~Wi`udgYbI<-1x*RRA@0*hJz60Vw}vdG<&>KZlEs7*Jr^)nV$Pmi zaJZTntEWe5$$?U|Hy|IF@_D0%Qjyj5ATQ%7YO7OR*Ma?Nj zY$OND;U+=Mq!X#9-HnU~OMfZUODira<57h`9#p~*2t9ML^ohb)&lP&0E75;BW-CT4 zT`5-|q0d5Kuo4-qCy-$rf-73mTe9_)?W*(?t=$DnGjAq+u2d8D-hzLukr;0#hilQn zN_eCzHqo6zFrs5YhN_Ge$2dEfo*76@_a`TNiQz^@>Y?GT;7~0_bKGXeBUNkIeNZHw@>uPE>YGckP;OtRKuS3}oi}(g=9t9^O2BV#mbs?c)a)`wwm$JhW*Dp#u7mEhE@BOlG&t z7PqY{Z<@|8jHTvBlbdG>yEoK#EL6A8SFlrxW#d$inu`+|23;J=Z5$~u7P5J)#JG%( zakeMHB)3ggnZurS-6+`C4<)C&LlbrXOiy^S5$Mm`db5_Hl5@5}AP!kKKBNf-;aB|=J`jM^uN4E_g+|;{e zB2Ve=b+v6%#q~q!$wp|f=xV0SwV1vV(N?3nM$%LaY2pq|&adgq`MH6!eoM-0Nw7NI zX$|ljn$h6U8m$`6?tj5wdt9LXQ~3o=o>K%A1Iq{sjQq{#?|=Kn2gI5IXE6EZCHC!} zVv#mXEg%}I1xvw6I270fZ^2KIVn>I9#g=4&vCu0`U{yh>G@&8&g@rm`HATQL4FxRI zIXFy8hX{NPz+w@E3)n)qWLcq2TZ{sAsZT%X5IPjkxacgXhJvXNFVhwMQzSqhown#y zV8YCViBX@4{o?n&p*NZ^E{qKJ@&GBI7#xOz#fD{ZgcsN=D42!TfHMz}f}^P~;>gf6 ztq|hT*ja;(LPJ(}(+>}v0=*yJKk?!F=iueb7kA&he(1`jeP>VYxPIl}!+U3MUONgV zBR8%dyL;>O?HebrUp{f^(y6m&kDfk#_~ONrQ1jipS8v?7aOu*;Mxz*u1vm;1Mi#s* z*je)OEBC-Fp{keru|f|0=6OK!@^91w9r9*}i^E_2rFA&}R}9NbalrQPAMd8V7N34! z-ca_0>h;87?Pjx+LvHOBufyVVS{xR0M4=wzDsff9tl3yN zp9mL{5hS08bX8KtTr3$2vVRjnA&YTju!xKf6~~54<0IvX(F!s-Rt2?SI#3H#6i^8U z!g>e_MuCvP9hlb4`Pt#fu0}RUa;6Yx1a;;7M3gueh^>(v5QLFZwm>2r9K;SvKo|<1 z>d9}O?Ab9lxOKX3VYoKkTNtXuCz{!rz9L1S^k6wQ+Lco5H4rt{QwTM4f!<qK{Y9O7PvS;4%d%!)aS)w#@p*G{b3!QymZ4jitq z8aZG9nW1vVnjbFjSl5rBh=CyY(WY;#+po$ zJJX*d-7sFmGCPn5b&bYE6< zxOJv``&_SpVtAS1=o3K3f&kGB2eX6W;I@En*qk1A%s2OL>{s!5(^LhBCQCISeXJUs z=n8|PpfQ3n)rgMQLNncQ6pBa%s0s@y)zG*Z1^YYJ^&ob-Y}9D#63R{pC3rh^~I-}5VyB4XNQ_;3oloq`mE2? zNZIH5^6Q35gJpg>;SS8zv&noo9N~y>PX5+t{|R`Rs4hHCZNpRe&@%ZlF}P}GA=clAt`f)l)+x88Q?`HG%OZcfAjfWU>PHu zm$3lLa5NQ2sq^D6AHdO|Fzh~ie)-wu`?n9BJGJTY zy%QfkI&=Nf{yWzX-@1C}!R-^buO7W{YWL-{`ySmn^W@$+;`k?z?>TyS$E6ELZeBlq z?#z4aalU-<=!s*y+2;J{!Ij_rov%Ls^!`9!$>Y-by*#y<-OaWYOBA%qcbxVgTb|K5 zww=S-M}x^^W~Yl%4ZXdTZ2M0ztphQZ5@BbXzH-9F@}D?2V>ze%m-93@ri=~! z{GliZ_^ zPgi6?yK;^_*C?)<>{2VWW}6%Fn`lnu_RiHlm-=sYyHuyWpXK4y`qVL+EC>ps+LxxG zwrwrN$GZRwTGvjvxjB*LF6c_M;E=3 z(%aKXs+YjkaAE|f=`EiDt4xEu@~AeO@`5fqnfACsq)RV^`qDq!o464AKoRp4mZx)W z-QI2Q2_?^6&k^@4jOzQqW@D}5()&U^8^3k1A7d+jH0OP`^ww#c&(lHq4Q=_s6HAXO zZB^qIP_IK#REd&EG$Y^omiVfOqg*+k=15IR9)2K1zpLQUQRTLFCsD=jDc8O;VWAHc22qo@x} z5N|u)F&4AF?RHi!&C&YoP@{)@bP9b%id_`>Y|6715Qjl06txM8b$J&jimi2osdx&f zc*rL8#X~kp(JA6USJ4S6&*;R?`LXH~Ingm9Cyvw#-MBILI{|Oh9VXFLq#>UL)wbTusM!1TIrvYX+1XOuvT$oD`(zvd@Co- z#sa29$U+))YeOC#IuiEkqkbdCWZ0Tdx+}RrS20{Ggiy$$H!HDn){kCDaab-(3e7Yj z#X`yuD1+TuYWCDpVnlJQu6lN^INO#(bZdnOrwMU@56AUP4tJF@zG^;5|9$mTeF#1R{S55m%2~S@oG1{FcJ2g<*GB-3o)?66xp6oC8 zmgCJrWT>8*9<0s|clFjogFP`0vmNS<_cg-(gz);4Q=@s#xgF?^_H+eF$u7*)=BKN( z6Q%jV%v^tJv=;2kyGS>T7B-ESX8V#`r>Z;W8e1kSGrft)W|V3RL)qz``1C*ynHw(7 zkCrFRU(%0uK0R_umgonW3>YtM)ofb zZyc?z>(66cWcMhmVH?Srp3LU)u6^r=7TBuUOW3O_>g<^5IlN_J=WH)_nv>nVu74Mc zRHwVCPo4F{xpf0+=6QJQ$jRMP2R07un5}Q0>Do43+qc+vXv^^a4gHjIU0l)L^}R!7 zpSTt-hiaQA%0k&a-%XS8dU&K7z(|pB1wEh)mb?Q+4+HI5*Q8r2)774Z9y&j`c?d@> zj^r7I%BT0v9NRHYdiB^QQsF>Gq;n=l6PnYCdA_%4khUmCwv8U$KF0h>&+ebQaBzWi z<5-b7Fbb{ke$vRMYv8YcS>eJ8hof9Zbn5$z5t_49C3iF|d6T2o6dYkHw@e1<# zGGYA2&<`)|qbp{DaGBKEHk(~P8r?7+Ul<`G?3nKJEsn*edcEj#gTeXX@OaZTRJD?> z8wydg5;ZU@u8ZEdSneEV$mE$eDK`%;9qS6IGE(zw9MA4uZ0wk;%niiI4wM}IMSE}F zI#_m2Gy~JU;jy|8<8alDPKVr+Dc3_RB?4D&4YR8aM9hHwxaiE?R^WlL#O_%s7K$7EMf zbTU%nVpF4CuHL+9q~@IN<%c6b>HJW1>vZnWR(?S$l3f^%ZJx{^yXMR52U0@?7t@$& zM%MMl$E*H{T42La=F590_5uFu8b>w{QfJ3h<@mOdh5qEhO+7O`p`Ekk!&~~cO%*O4 zoOyC(_x{DkgA3blp4@P5|K!na1B8Z8?i!)!#)-vKyGK8}bLh7|x$x1A{rApo{o>w{ z-}(IVZ-07`>@R+F=U@N9$G`vejUPTfiJj(q7rM5LdP8?658RsWnEtgrR2QxUd%;O`I2Y0cNLeopU6K_XO(_DOQUt(KA4NojiQ9suf_7$b94SfqOTP-njf8 z$b93{p=%f3yT!wrZ!=qE|Mj&iCtzfF8F1dZd3x85`3vU`U1z`cC-<&iKK{`Mw|?;X z!?ULkK+va7?46sLE|sgC_Sa~1cXc(KPB%9UIAF6m6i^YT%eB5hFCDfiPTUt+*Q!Ou0;A^^~x%guO;%HDYlXjg6 zs8bTxqFf!k-?|D?biveAbGNo1krhUM0RY>uEA^LuA83utGwl>nFMLU9@vOp+Jo}BW zt^97#@}9zet*pvcl;UB!LKu#8WmnH23UA(LgkJ`@#udirX2lRszwV1K`pc<;rLBkY zk@ApktIcY&+mPpWzGQsvRF^wc2WmanuMM1^tso9H6{i=r4lrCgMsqml z+?1!Zo0Vr5GL5G=ru-<;!Z9ZG?29O}+~E9F!-E}_X}2oSoOcutw-fL9)pLNCdB`Jw z;w`Mu<@0)8VWfXv7Y0Ja+rQPx(ob!Cs;rI~;^VatMFQbluMVF)D_>ta!baatC!07GvPN$W zwbU`!uK2n2R#hg^`uO344GWjRJJJlKtG_R`z3sSFmtPi?rxT`~N%^5vae%e;pg}=} z7LJoev|9F%x~*0v+uBmPr08^Z!k1WD6pS(KcJcsC!jbHS!Qy@h$cZE=$T6-N*N@{lReQ3saPo!DW+rjWTN$~&rCI+ zt>%-3R3sB)_hP=7PWM#v1C4Sem#gOUjZ(glO4SO5W~I{KXbkr>k^XvDqg=*VFO}-0 zT&<8QXVdjkzOSBdR_H1OmIhqP&y*l@Cg$bLTL6}4#K5P3Feq9~d%@xEY8*rbNTJ=~ z-u!5PacqE-W_dUZ=i=s(fhNb~=EeuhlfzZ)P%W)sSa_CJjdB#WO-EhyGdIzk8mVza zZN0>)wn|IfbvfjgvvXl;&f2BI#87Qws4~)5pf`l!7zhh8J=#T< z;TU~(ys>#O0-HY4iN5*;z@bbFR z26iZT{cIPqZmKdlk^`2(XJm9BMG7pV%#0TyWlp|@rhyR{3K)S0feyd~{F!J(050$c z1cQ1I6tWn1Ec9=m@7uY4VE2Zh%`;7qd8jK2Ca)VQ%?=gFg2|)J1Qs~DuN<82PH&rP z0=&>MXbjSS?11-ko+2 zm%~(GwvayD#GHXvW+*NPJE#TWa%nI!b+|yT4jl{FMOMN8o;cVAd~xlNS3PM#nrR!Q z*kL_*5DTR+8Wvy>KY)kuCSAd32qcTnSa2w;in}NkGhswL3^roH4%mXFbb^JHIt;|j z1!hu*&gp?VSm;xX%WP3}xLEPRkuqmtp6bi){^Slc#J0;Ly6(_5#D2R8K_ z+c|uE*9Z>5X$Y6h&lLRHz%KwOr}j+qLjjP^j|?FHjgwn%p4xWp_?9Ev2aoO;I<w_!%e{lCWmQQXR`o%X7fAP)ZU;gN$fAL$N{L7z{KKc1)cfPuR^N+v%;&(rP z@;jg2_`|R7{PDN<|KO|J$Upz_!+-qC-~RKTeelnIaQBx#dVun0zy1Dy^9P?R#@~DV z%ip^D$3MOECqKJS`upEpC;eA{_~GCF#n=Dyzy0~&|K&G-^^1@H?qB}!-~aVb|KYEG z{2%_ipZ(1*zy8yoKBnit{N0cL>i0iGN9gMB|Ln&Mgz@kG#gCZG-~Q>h|M~BJ`EUN{ z2mj|k{_}tMS3mi$|Nc+@pa1yhfBzTX{^$Sx7ysXX`EUQHzy8^u|NP0{{K=R9%isLo z|MfrqDS|@wFMjv^fBUCj{jdM-_y5a3{PI8io8S4zfBn<{?Qj18{eScO#}e0cWQwt;i| zCN3SEIlFiK$d;ZR)A_NgvlP_yCUyO3!(4BW-wlQe?uGtzZ^qF`IA(iubG_xo(VmTC zgQN9Q*6$10Z2s1_2=R~U)2{)~{1Yy|Y=3uNCb+C_VJ6lro-IWj86%|3@@xcZ7IEhv zfBBK%Stu0+0aT$*@RkMDgq#0=*6zbit76L^J!1fsAW1~fBj$)6bHJQ)&Z43yii(1g zljPh$1D$iuIY(%k91Q22ncv*sbD#US+|S!f-|2B??w#k}>1RFFt5&UAwJPmh-(9u$ z3y{K?fngz23b9QLiqeEkF?MyKQG8%nU=~=Gg@iIYv3JZGq7N@ig@5VG2rS#b2BoQm zrcj~sMwBxFmgR+!X*zCA2yC*0I1IDfxhAv$twcJqlk*J)UgmJZ)Y5tIG6)Rw;z3;u zqzo_HGRACMzX-*_?ATKVsHMg^Z3nOcWCT;g&eY;-U|_ZHebHR}-Fq8W&s()bxo@xd z!56lDYWss#i$4RU?Mi?D&&6Uf(TDaijci(&s&HL;V;j}4}fVck+8Badh zAHQ!X^jnAjRj%QK(W1>#(BjsDG~}Ygmyn5t>K{Kx=$Q|anpo=n@fXX>esXA%HH;FHp`WYv6X*X{J08T3P5f7$hkSU|qRLLzNg_}Fu;c%;g-E3wjB@`geg04j z3j0ykaQ!&f$4HQ2mGJcUyjQY;%s01Ub3lbgb zB<0EypMO`WjU{i4M$~YfGWI+kqD#k}kK$XkQ!gIrgS5U_%k$|IFZ5+v zY9!ZApE&u#b1$DT`kJW|ufBA`6%)tcubMLc`stTkI%XuAI(F2wi!Yo${vvXzS56*x z!xfXKPq^Tk%f{bw)zqt}UW~cz+UYZ|nRegIYwy49x_fWE=B}G&;2*m4CQ(Vw(E80y zzaHXeN8&d(ouPG@);ZfaKK%d9)D6W%xTD(KIKsW3_Q zRmnj@${b_LL0@@U-~ey0oHEL*vO-^HbvD=uVV+|+cI!10?!4*JyKbKP*Bkt_=%o+d zapl8zUp?#28BaWL!_yD>QI%UByYIStXHLKU`pLIlH%a*NQ+GaemkLg!^T<8d+<*HO z_uY0mhO+F4GMk7^S^S2JcJM2xniS*rS^UC_+9^7DT$zVaeGaqF6d%}B`b$sV@xtS` zW0ZKujZ+vDKJeGSJo(^_&p&qCi%ePRQk!DfixX*#mc6{vgbC+hw=kF!83V#@uqvICR_f6Yha-?zo)%)@v`mX8Opx zuN(LHoztJaALhFBj;lxi^{R{Ry#a!oVykZEUq(s2??yNgh`#iJnRIap^@P zgre78I&$jh5!1#dh{DA46Li^_5i=&8bHk;huAZ0(ubFi2)C)$;81HoGsA(6CxPHn= zF!_G(*j{(ZExyNlnO8C=0po8yecKavT^@d7p1E(vQ}_Jk$-A$3{=sX|a}QjtY9S;? zvu=ePr>NSEmz|@gDs|8G7pvVn&;Iq5M{k_-`h%ap{RoEH`5!zv_pOIzfA9G_zj)`- z4_~^6SuJqawPRWM;KjRNe&hzJZ$35i(>G>O5PtN^ebJc8efr)jIl#`xuih_7cJ%%W zcfIkXjaRt-N_k(Hb?xg<&-~z(dp~(|7W(wdpQ;x8x&Nk#DuRi+L{r>6{k$0yM%;SE$h&U1_?k;c zTy@C^%wu<7{?K2iV(z?toN}Ljz_Dj1#;;HiA6|U)hL;|@@#QD3e`eN|Ua;BK)orOfbUHitfx6FS1o_QZWy5#FuHZGsNb@iML%Rbz-?(5YHKlp0)GatY4(5G)d z`pqZL|M=zWOTT_=+v<6nmVdTx@y9>?_|Zm~bk!F-)_#S`vVO|gx#+;Q?=Wpud%EiO zch?^{*ph{5D%)AGe?{8%?~Bt{6ArZH5VltC>ut$89EtNPnO-RbV~U;3+sl1P3#R4aPCz4%Z=&c7UO zI@VUyQ=Lv3GuWKpU6s~avgb@sg=k-GW_wv`NBO?PO?h(tpQk(enz9bJ=bz{;J>FB& z+nCu}xtHYh!HSc8WxpM*BOmN2`1^6*wu)lT_Esu|eHIw!7#>ryZ*AVLH|<8X9OQCH;wr5tG~INAz6r*%~99c;<{=cDz#wfoQXmi+Hijp&y{ zCH)QiDzjFnZJED&G35SBHU@2S)hk~5sKyE-_pcMiYN`gIUf~7RUUYHk3jn{`6 zjBVcl$gpt0XF4*<4)exlG#D$&NF=ez6GApafu>TBsGyzxzc9crL=67|#ZF=i$%Q0z;Ojlxmdj|n z*>|u*0b?f`SO%^!)TFY!Y|)3n^2)^@FaN>MRDHa7!3Rseo4s~fViy~rhL_haoxfq_ z0;u`NZ|1kn5gpAR^{{l=58zVtBQ{N?9w!^_`&GaE+svh!;%KSB;NL(nfh z_w)zvyt!chJa~D!?{UCK=Ykt!5?iE0TV;03@P6hjqJGaVe(Wr{U`sN72hDHT`B}Y? zU?z+kH)hP3i!PFh4ZFSnZp;ns0ofh?T?!i~arD2_8;Y>tFR|H^_yuXYzwguxZ8~{k zuqdqkKaiN$6DM<;h!L_?DNT%Mh(;VaVNl~oZQ^B~Ir~xgha7afFF4eA=!W+E>=LiOtBNFZdnoSluoQYf{oR6=DdL9r8V=laq z&&h6LE#7DweSty~O;1gz=;&Dt<#35|7#VJ#8d6Xs4G*cw61fsJqV|V{;cA3RNm?G| z(DfhD;cEXMcrb1J*fFf>ps^Q?R4c zJJE(p6gu>11#bZOgzOM=&iFBjugK!Z`%vsK5SPp)DLmmGf06@0lvmLdQQr$a4Y1)n zZ14}cofy?aZGKVYy>KMw6Bi2BjKJ|@JxNGR88mtPMVC%;Z2A1L7mm8(vhkD0UwHB8 z^Wj9Nn8#goE)V%=E=(ME!Gy7+#*Z0w@#v8h#*aB~!q{^sj6LVFOD0X5GFdof;`j+; zM>FC>xT8m%mv}w%Ldv5&vcRjm*E8`G$6csIM43FkqdR1O%s5>zYUH?!&YwJf)Fl%x z9PfhUN;PiWv?&)~cFDL2W5=RtQ^roebnGM_gPk$ z#rh4DX%j9Q>!ZHsoj1v;jFIPF=r5DT-f;Pp>!wW>&b<2a zJFcJc*Xypl>&C0^yY>2~9=sz7jX`GU_~}`9KKt<9Pd@P1C+@%fk-Kl1b;pgu$M3uC z!N1-BF9U8cCJYO20>YprWJd@l1DIeY$SDNzDB!P{e13pXl0d0KNKnT3pc9npD^I@l z*SGq1Vy>ZxOHxMITX*IfoUJ6#MlJAMo!-{nV0 zue#*gY2zqdH~nHf9e&0OvmiW%ag3rb88c$qgme6q37aH;8TuL(f3BQz9@KWjU&erM z=;o^c|4Gnp$T2s~@O|Ksf4Ssba1VdofP0-lKl;R6ZgkJOX@}@5@J#PE*!RwxV^yXI_7=G!#*Pp-pnMZDV z=+(;9_zf$*T)SlMrd9I~>|9Zpu^HtZ*pRz_{pQtQ@7b~# zQekYXEp>%^n#&H<796NAI?zy@R$r7^ zlb=;vklj?8*L;miygk4ox{q_06mXiHV zMXACQUB%rs2WoORm!Fj`{gu<(a;6 zQMoX`9I86mT|#)Yy`ZgpkLbS~Z1oI~)S8RB z>(e`G0Ga(Q6?>zi;LKt>154_Q}32-!+w`ROD<-+rCKZu619L zmt_1TOyBWCW#0Od%+)QGyAHKwb=IY}*X-%4-`7;Wqq<;YUGbKN(rsT{^m@=ywqhBpgAC}ws1>T-q!Z&1M*hpZmBEUInY+XvMA#{4Ou{R7^f2i@paaM z*y&)PvRDQZQDyFi0Ev=dOlrA%mu98>*jcf^rFeH$=BDD*HI*3~;ZT?tL&6t zt1v}*m{fhJrh#gTveRr@W=U8{~(XJv$IikR_ekX6Q z-iM&(Q~l+lh(VUQ#4(nkcm$G@!{hL~r0nfWI1spArCKUdwBC`84s7hN|u=|INKYX{~ zW_myUrbq9(&Nm9Knmlg&M5jA{KO_h*kFsF0i}w59{{UY0T`#!U`^#W5L<}{H0@3*R z>Q|)3d2Bi`EQ(EAxD_UaD-j6-e(fp4r_eMtU=}(Kp^yTI8Yq>HRH!!aGUUq|5`|lZ z_@shVD3~dDHcEkc6{@U&k{L_m08`mswRQs#X-tczw=O4ead)xBmKYaG}=Woq((tFNp3%-0;>KC89`qZqu z-+bZ84_<%a{@ZUHzPIuxKCzQBY|q7r^HX|m^ph?+_mVN^PZ^sSgTod4)?P#tFm)Jt zZP43-qrq{HaQ?kKqU4C$$WiBvy5L+PO1^A3JPY1?h<{qrC!?|WxA_uVgdvAh)GH|I zt!UIPm_OB&8cF;#VPv5un#>*yo%kKYq@09iGWs8x#Or=uskq>ukbR&3X!B25!aZ${ zlto_=e`@wWWf2|yA6y--6R#O2B@SEurv?dm=qDPLh!h=sGGj2*6nF8c5fd&m4*2O0fDW0d9h`1}+jVvvGgW{h8>@beIU55mt+gy-C*z+nz3g*9>(P$(#c zJfZeSJLry>0?~t^_PerQ()^<(IXOH@HQD49KjQ}?`NOMl8$abD59deSa^(acZuLZT z+PD!jCXe*hRv)6d=cehm&X_3G*IYd>z5iBUUzvWzgmZ=N1y@cwKhz{q^Q~9kePu_~ z2W^B5`Ub3@o}51Uyr~oZY1*W7{Q#xt<(Hg)*~D}F{)Jz=Kz?__4{$vF$Zb!|y2Vdy zTsiduKOgBwG4Oseym%6y&3WaCyI+6$KIBzIKN4~O%;}CbDx!j_roO@ZR*#0#0@qI)Lpbv)ABIhQuE)oG zywZ3R@G^De)t8=s{qzg{dgCoK#*z40iW_ZCO~3F~cwxpk3fEpXiv4RRpJzMi4&S`> z3z$G1l;WbZc;d6VDyYyKFCQnWp&u?p zn9n|ZQRsf*2H`l z@-Z{77;C4@Tby3@yyD_>W=uR^IOT#7Q!g5U5sn>^;3u9pV$%5|ri~df-LBjP36g5V z&JNx5aS57mUSh-UhG`dGeaU%Oj8E*=T{-ceFjq}J*Nz^3@~DIYO}l%ST#(@DP#{O4 zDWpJt&6M-mp^2F}~3N{CXF z%VC>|vM6Q7Nz)FKGJuX<0H2|nft&@AAPz6VSG>*PV`|P zUlW2NAGu=&AQ^D-$9Zqtb(Lhl^^5nO{raPq-hXM9F9P|nk8l3?$d8W*`7p0f48`Y$ z?zrw^9|iK!p-6q@iCf=)`M$aDJ}!Lev73D$Xwet1uKND{`5!&|!7KNF_^Kasxt+4q z<=?)u^1JtJqJ2O2<#``GHTRvT7tDEe+1KwYan74haAW0y*`L1kt4=v0QK9Co?G?pyFV@b&`DGWP}l$PQ6zo&-jf>_f0DCn*@3N#QTq0! zIlEWo?OlV}zIy)JMIRUJ`zb$lZSJ1cJUABtOfOTmZ8Ia z)u)a&|9ZMJB#kwhCl59J%h}%l>z|JZPYpC4?X5X+u>SPnrncIgj{3Z=#)9sq!k*@$ z-j?En?PZ6%Dt|lD`tv~3KaO|&{aE|S-dfTBb*dZFT$a{emDSf+c(k)p^jKHbp_Y>F zx;!uh!JBXgM5zj0b!noYWpnv%Q9F8pI@JXx1x2Z;GC)cbYylqWsHTd`XzwqODNrLj zT!}OY#44&3*5DfUwHP~jsVfQ(tgg)6r0I}oWTz|(9eH#1EYqH$uUw8PTA>u$8`3(P zGMZ~rD~q<4=4~u5*o=y@*QIRvHf!JV`ihkPjyx17JIF^rEaynR{0iXKyac+t$&L-By>`RCS=aI_+Rb@tGqHXt1Zet0`AfP-kOKdwmw2 zy0X2fx;Uk>aA#G~uKMy+xfnzp4Vks2Db>Y0o2&Pu3XdT2w$zmD>Tb?C++A|8qp+ps zKx1WUdwoW0ZCX{)4*oFMSh*j=T2D)UZ)*Yfl&ClqOm}l0uMT&YHB{_np{psow>7W7 zy?{YxkM@G*%n(C3RG1(U(1<n%Rb-eL!@ShoMHcc=zmBK9hVVpsh2{~}AtCCk0+1vMS{Gnjq`qqkc_K)>f zsN-Ny>4`&C>UI2J<;j6+QPn3u(pyGCMnFIs|SAo!g#u%8Fnp`j}VI1cs2ZWJVi2+{ODL}8FF>CBVxH1B{ zu{A7-S-4n6rDMMO>>bvqiGt^hvp|Q3QW=q*W%`78I*JSbW3QPfqR2invK?#zV|N&2 zj_qUU*oHDW;0*l6wlchohnE3iFd1I9n=B)kEN|>S)1-hf35dP=n{PgQec_kyY*_Kt zsvqXU%d2c^dy=r|%O&4@`s4gLE54t%`Umj&OJI4`LOWq) zef1vr3`C25^Tpdh^p~H$4m*>7^w!I>-+bxAH(q@9k^64D?z%f}zWM$;?(j;pUn=z5 zg?^^cuF552##}mXtW6d_To}X1yKEPaJTD1~L+A$$ZOV8xR}?PrDr|zeF!2g!@(sr% z;2lEEqsNXL>z!E&Lm2sd?@?lYAHBoSv|Kbtj~UIHm-XW9SI>pcADP(OvQ;Ovk!M@n zCRglp4F}E_vHHgbXt)aOcv+9D$ggdUIXCgCc{KVQ=r!@nj4D3HR+>H5Kk%{|g^uvz zk>}FU8Wf_wk(2;^b`pm)$N7l;Pzyv_Opil zcBE+H7a_-wpEkihksnDMd&!tl6E7S&W$c(4Q>V_n{`z}vyY0?fpqrTw+GcP^;H0FgTpM3SX=iYqjrPp70;pJzZne+bpUw!)NqWNDf zS@6x`1q+sZ=j#MNtX;Zz-;NzSH*MOmYSpU6ixEbYupoU;*4|zFwr|_JbxZ2D&AT_R z-}aLiF;;E)>8H(W*KS&~25nipcKiCZyEd)avvuvJ)hqXGU9)%Fnhh(LtzNQZh`tkSFG5%aqYex>r=O*jTw8k=BMv0%uLDLyFFvi z_VnG`(^9sj@7|V`x+C|%PDzcGc_lg7B{^9r=fHvTe7D!)mYTwvlFX{2bQv@DP);e$ zO|2|UE6dwgo}b=SmET%h)KHPzTwT!DR(-Iew!OY&{ucdTEvecg&3>sOW+6qM%W z3G2$s+v{q(n;JSB8(V5?YfDS2ii^wgEgteJ3JV)5D!Q5yMQ5dM$=pFAwe+Ktak z+fkmsza%52p(L%dHov_(x27PqENf?V{@&*D%#Iq5{XNl5FU{OR(%a|(a?RoP$~_yG z(67qdvvvi*56!8B6_^1ZlKNXKq(>5>5vb!mmX>>>2E3OY0U4c z&rMQY@!sZ&w2s>B_L{7w@&mO+dq^5e_o-J&#`f}T3(J%UwMJdh{Q9*|t1@=2%-OqEl(68yhTR(%?f7Xy+K%P>wl3MYeBP!NUt$)2 z{?2r&py|Ltrw^x5Vd&w7{?p?Pqch~COYrosFbe?eEh99UYH!pQ# z!T!x0O`@(0hc;&iLubiKo++QNN^&!T#Sys69)HH+|a?W1v`(R7?D zbTk$=+TN-)>vAV9mmB*DRd7bpCsQ+nR-Q z0JNQJ7wr1!`#l>L9oV`Q?Owlf=h{WP*Z-KhX+`?>^#%Jj7pHAHuyxt`#S2o_t;pK7 zx^Vx7oZV|PcC5}$S)HG{&Z;asWrLl;n*0>Ig!{KF-??_#{w*sc(aGBNlO&5nRFl8k z9-=j5cU^Hubzy5o?hxg*Rundu6*m+YHJ0Qx6z9|yWLD>;Rp;4QOz*78Ni04abDPWa zJ8KL2n@y-}+eS`WcGnl!SF)XCuPU~X@THj>^1aQMvBfIU>d`t@i59f6a6NOdzP@O0 zRI}*7#_GIXs4{2A;r7B){gtN=R$**N#Q{!dI7J=fl!Q+7mfI%#Ww7p_PqxscW7CQ! zbntT7)v{&AfrgS4^6vUf!b7dxR*P)QNTtVO(wg?SBQ1woiY;q7X3ZSS-P8Sb*5Q`B ze?Q)&Lv{w6p5K!N#0{W_y%r%_X~AO82yu?G^UaW*qA% zKHgc$zuEExV-wxNcS8*w`w;}g%Tk(;O(!utUzQ(+c>Wou;)yKNZ5499_ z*JZa=rZt!CYbs4`EJ>UWwdVzY!NR{Dsr_ZJ#%>pA4158Gl7gVK*c$uoOxKCQ#=-uY zV*~Za4>w>C=3sY4M{_|_ZB|=j-l3jKsjzcTdkGXU*j)tG=o6oacEtxzt?Ah5iuOafqn$qZ_Qza6W`XcKC%Elf&5I|$)ij9hFg6k)Wa z>{9V;(ud6@DU?v_i591?g{)=t2!g&O5EW>JSV88d>b)>501H7^6mBWVTw9#8zNK~_ z1(3P1Y7ZzaSM-ZgxTn_?suV7XA~j_k6J2&s?Lx{G1v{W-*conxUJrGaP@onl7^I~PBL_5wni-)+C`lBo z25E($alm1y4LYkY-vi-dinBM?mF))KU^)trGALbHxE<(bsMmCObvjA zsY&43q03z^h8mKDz-M4NCOb%LcpK#Hb-4z-aqeLeu#5@R9E0C;B=DI|0$Dc~g=Gdo zc+T(yb=T!W>;E|3?h2!bz|j=oT+act7__T`Dy0E~hkR9#0$dz&k01bNav&N;rmyAc z!^K)&U$E2HmQP2h8D#{MO*iwdwIYKLLq+Egv=$lChN4Yv$L9N*)4Ln?w^pUp6mDuN z*jk&tp{HhFeg5Wx)MZ{3|Ki<;KYaPF?>>7DWPbk9YvJXG@16F-lQ+El%+1d|cGGgr{muUqw%)Q{eK`n4DDfB&t=e^~Ir zf;sQL_u>^+#{L_3R^$-+jw{H(&M8?bkE1a>3_cefY-fPtAJ!xktbKPnvMv)F~I`|JnQBeEjzO z*>9$9TBWjDX6wpDTUIRGzWT@A>z6z9Ri3rCIBi$X?#&0buBEW+r=@g|R?1J^rj;;s z(zmZ;A#>-3){6X&>Z0bdT&-G~v0JKDoVH!{-4{F7ET(|=Zd{3E*V@z&1w>!STJ%Wa z{vA<-XiItCp_b|sJSZL2I`1Ytx8&m3$&+S!2VYpgifT!rbZDON;pVdQ1T>TM`L-rb~gb+P6mKhjZ8GSFIs|CdwU zza4Em-c!T4`l^0K&d#!|?c7jPg+?9mJW=Ao`uxL9MMqmp5k|DP#_L7dm?Q8`eSULM zYI|AQv9_{PT~z~(h4pzU1^+?W)Wa)?|C_Xm?}5o)*ngkPl34V2v-I_;!l#oCtklC3DNd zob5jn9@y}$525(rNln(qf|O;1KD1JqzAkg?GA*wc$_{KQ-M>+rx0N0c)d*@Ap}vU5 z&reyyf5ND~uGCpl5xLGnd{#UD%c8q=Q+-LQ2L&2m@8~$~tS>M~`y-4V(Gijk)n|5B z?(?;lo~r#m|03+KO+VJ^3o2Rou8P!ybs3_^oil98!CQ=->Z`TlvT{S1SRno5c>4)^ z6J3>7Vic@fET|H@Ap>S*F@S(9?CmQ-2f2HfLj+dNqJV()GX?~p38Mu_gb=;)_uJ71 zQ5X<>1KfZ$3SJMyW8gRvYF-Z{p#vmJC3GYbi2{0}fh*}S8%Hro!Ymz3C;-3CWI~;Q zFbPQ5+mz;nrxn*gyGJp5thO?3c+oz#g^1A9WoWR60 z5(KOe3pgf*ZW*vhsY zd1TkHfv^~1A;3{h!FEs@g63F7&Std>9~>X$3B(;$j>?KcQkI%}2c8N$ijJOAx>Pub zWnIRxguE?3W}<0m)_8xgwKT=7Inh~U;y4>w;~;MN9y(oB`>L}yR^{&W+`tnIt8R;J zR{^!H39tO5n?dRL!j-rlQ*S$TeGuT;tqOameR|%n{K4Y&br`y?G zXM?K`s4SjZSAC{>(ZNt7VTyvNEGiq>BX8*AHI5`yp0l~Bd~etkhFVMIKBeQ^ofED$ zV)qFdKYggW*(v3cl)B1x$Y`z0^T5_aKYVjdjz@x?)_QE~ajrh{BoIj|$=!9d zzvfU^d3Q@;dqXbjX)QY3Q+fQ5jxO(OFX?J7=xoZ5P-;tUR%7*n{?6iop0f7F?3TKW z-u6P#;G1jHdptxMs5#tQ-ddkYc=TYEyo@u&;E}%S!CphfILXJK7^purXi!!6wiQ!5 z*jbh+x4X2vHJ|0CnlvGs^_Baob-JriXEC#c>{G+EHDm>J1!Hag;yu-|+xzp;*5mzk zhwMA|RR4COgmDGlLPICyBq8X z9O-TP<#_MU$9gd2XO47Xj`lU5J=#6k)7aBuYq(u5rLYEx06f!=bGUrx22KGt~TU{!xtNw2fb-yGwDuj5N`2Tfw@YJF9{?^Lw#?tn>{HDsRrpoj-_qaM6 z(|Mk^c-Ye0meuF#-LS8=Y+F3#CG=r(zR($&2 z^YdoEwt3a}j$yC(e(q08zFGd==XRDs<{!TN^vl`reE<0zU^!*O+HGrAfWY5>{qD*o zpMlAsG02Qx{Qd0tpT92Y+b`exYVK=SF zz*l61WT7rI1CtQh-D7=Qq1LR^j2?NTg2!s(O)Cig`z<4wQKRFDj0+@kva;D*;KG>|g*@HWeYGDlipEs&M+C#f-05 zSUfk#$O3%LI18eDPy~bvs|o{>(?@ug3C|pA&85(BPs50A5H(RMSaS9BZ;GE1m!V$Qbs?xgB^Juk=&>_0V5{N+BNl z1DZfE{BMUE&-T`e0#oD^$iXYf5!wo*R+qc0WZ#DBtnGx??>K(eGRFzdJP35=d{ZeJd zR`^;s#d8m-ba=~cm6AQ4GemY3(bS$rfW!Lha7UTP6uQ6@43L>n@BDVGRn+EtbwLW@ zul<#<@ZrX602xx|ZD8cnT_wQ2ik#{!{te#r{$1(rqb&)H1Kk|&uCjj$`?Qp&d8!ZK zkYEtVZ2JT1K|m022Vuc?Ldec`*gsA*1~vrlplLAKW)lbof1!Ul)f&5~Ff2d^Y{J4Y zug%r~$lz@tPz*#I_!lgN_(-S)CAx-23%N{ojr? za$C772;^!a4+@}B_!J-|31|$B;>js^+Mum~WkN0H+dEp4Bpyf5aq<>+j^_^$w|z-G zi6W44H3ewQ9}aj5!NYjepb-L4YZ+d_;E-}`4l0+Cz~GJnD@7704U7{a{q?hrFq>jS z)5PpBeauZW<-n!|c_~Xx5E!|oWS3V9%_C^qfUy^5<0-bfY*!gMp0NPS@$5wudG-=p z-cTEy2wMS|U>&>+CjyfA0F2NX@CB670sRg^0sVEykF#+Q8-x%j{K#jZJ}|yoApV25@en`=z$SsPDyxxPp#yBvgf|0gGsq5*3w5%i zWhmpd7`4y=TB|)Uaq3`5nc6P^OM4+T02W>btE)V^sZFmfa$t2YeLNquc>vx+-Ni>d z>FO&B)C?B`wy-ndKu?KOB#97n=2(65@kJEDbM;ky1_Mg#DGu;NHTit%NbTvPb*!~H zwq2DP09nX50Is+`Bh9m;fhr1wrv_U9;il^Bc*+4Fd!PY0d$OVLkf$8+tfSr8nDX?1 ztF3j}uy99X9$*X43ZZ5I7K1R*E;%&a)slC@6{EirjI9Cl+X^7r%HmzXY(v$4AUD7) z9=1Jwv;iKE?6B~$gSD`8Q*}l|Wg7GiCJ%I1updyJeF&O;K$vmi!LE{HhP08~Ti)56 z3nMFz@VC=#Kc8q;A_6>tT*x=x4saGCwtxvq5D9`z;brd;;9+WbHc~-sFuAcJy`ek} zfMpGyl^QP^hyuvy_`%jVK?f0^I@|$1lLU?inMwNFYr2~&0B2xaDU5JHSztTD0N~_` z#{A=SHz$E~a?lvOZ8E4Dv%%g$4?nv~&Yoz7h!Myv1frp1Fj;Ct^#K^U$8)^q+*S{f zhIn9^BmgyQqC)zNAMUIl=%^d)syo`#(AQelT$SBep{6-UdMoVaI}XuMxx29{rQQB= zoudm0r0n&1@6mgM-iy-Sr(Ut}&6l6L;gfeB`2531K79M07oNKIv4<{$lmjn6_T+63 zKX&`Q4_trKEmust+z&JRzK`Ey9r~S){|UV8b6(3A`EJ$MySM(7wtI_n*S4D-y|(Wh zNErh@gUkRmg+1HW0n`*^v>z=aeV+we^4&Z_zd}WmzN6hT`p!0ku(rIAI5i5g(_vh8 z!T{;yarT>zli~YzdNX1b3+a2d%KQCSbD|(wIKWj#q{a)^lylQkgh?U$vhUxuDYC2J z!ug*H6~R8>9~39d`A;6F&`ILOZE8y473>d#+dcQw(m*#54gMp5I`+7sZ#)Uu9*4SZ zOb48&Z}S>#_swkzkT`Py``Uo;hF` zM1-;^n#98dmI=T7^zFGHy*_{LyWf2AzJ~*zGkiPewYQ&t;D>qdlxOeQz2OHtfLSSP z030|AJPD{38+m{Oa&hzY&t=WMCVT(JwB2hR2=sW+V?gBSU}aG%9XBHvKx!^`E{2{N zx(p(`RGQ9nhO+aVp7**KGU$n-J6c>Mg|4N}917j?=zC)9CMYDK8Is>lb-1j#aJjy@ zJi40271iZ5o&mc0#nVVv*N}Kfh-k*M#H!*wd$%uLw_+Z(Urw|Ux*AK3>#%EMJh5aP z(V;-nR-fbg$+(c5GKO+IX}4Oi7h_Y#k(-L_?GEha?fnUFQDJEz$ypb>HkPAqB5qG7 z4!g`&x|?_y>!GWgj?i7E)+>W1Y3uTY2T3p)7F ziRZnV%dJE&oIcW|J2Xq&bTn6EReEjNK6e~UO=&9q6d+!`nZ4O6gL`F>=X^WYI6Ole4UMWEYKbLf=g`ZpW&ln(C#32A+YM zTu`YdN!-m1&gf_cS&I?H0e5G;FA2jY3V()_6Pp^y@YaUrj?j8Qi6(_9K(1V*sfD6a zLqRm=f@n-AgP(7F7P4SA@qs5Qr8<1jq|t^NT+@kim5%99UvWVDE0HzX)s)TXM5&ad-5?#y<{c`50Q{1(bktRxzouhht z^!Y#;Xl8;SD{VsK7D?TPlWE;XnPTjuXNrtSvL-c&+2O&H$6d9C0F-#PML~ieoy9_MJkCp8UxHKNEC14 z8yonrBaA{Bu}(YI-(rzwM+0F*3Gvo!T}|bkjb**9RRdl1Ha;Y|!kc=?bq2+JR4+_S zhrmG^M5?=h_B?y6C0dmO`kJfynh!iD*A7~h^W5ezSEHukq&CqqDo7r4R!B|nSSMrT zWdWm_EK^oB1?lCeJ2_MAXk=GEox*u{C_bA_+>X1IITiOW@^GHqTMu{=by2UVdsI0p ztAc+!-6IuJaNndE*DJlxxcN&tZisZ^8pQ}T*}35s>4Ia}(4;^@9wR5NBnDL!=du$Q zI@Qros-wjWR0Bzfw2z95s<^(^Aecns)HouWppFJ-Qxp9dy)49g@kFRwt<{4xtYOIx z`)T!@Z5@_(0p~qh09aV8^K=oj;%G{rlO0 z-%j=`4nR0@s4W1(q0YJ(S}_Qv!V9+BD3Cx66zB^LNTe2o;k>9Ss0#~}2|1}9v_Nve zYYOdSZ16TfmO;XQ6;w?vkF+yN_T3*%U8C`%{dF0_8xMLmx+8MwKAo7fBUN@n^Bs8# z;U=DZ*oAkgbg{CP@6L|5*g!6-uB9Gs@H0ABEuyvcV_E)A^&&Ua&3#F6A6Er(-J!CQ zkPC?Viju1%M#s5X)7M{8O$rh%5EX#A zx#Ge_HGk*YYF zCM=#cnso54A#oYO8%tr912MH}B0h7{QzJw?YE6oLMtC5ovL0^yn z)W4qSL-wX)S}8h8#6%OZMq*g9A5JpMe{!L6sYNR()QA#<;zXYh?m2$Y=JZiwzr(hIXzXIx$nb`duke+ot@9#DOWDq>or~80_|6LtUHQ&) zx1g6E@#|)n`Of^CPu=0iAS4jEmxgUbq%!V>hEJ``5BO^vNSteu_Ply0{mcto3HohTNyU)m8 zHiBD0T_K?AZD`;Z&V`iim7{>M;4t(`nF0cbp=m^u0yqr$B8()K83BHkMItYT8oAU! z$DtO+70qTqX8Q7qg3RB2H5)(%mi<(g9d^-qbKd%D_Diolb@#&0-*RYn*SdvHil%K} zk(at2e0C}oUIv^EPA69p#(Dr5;U-roL23>pc zvEUbipRwk54$aaUR&_6f4&2D_@$8)r*b5_joQoh|A*g$}*OSRSfVZYB1wfATOOjw` z=bb1()6g*-jR&P+?Fc>EJvmr|XA_zR#x3=|bH|e?9s1ya`zBA^OgZ4L>P`wr(-DpC z5W1`d#dS|F=p7v@#K(2dB`)q$noDbF%xKTJ=eiSVAMI)7sU@`@J{F=$h!OWMni^1( z(h<6sF&lR@n)0%*c@?4YMRhAilsx(|N>^Om0pq^RCWYue3X-Cm!c;`#O73#4UQtt$ zp?WnK&F()M9Dxbpwn+%e0HAY29C`3V`a=9ZQ3_FN@QDfK$H~tmMtWaz7!5t?w z@s!D9y%f15afv+cGDE#!p__mz3RRrO(IBGyiHawO?jNH!(T}n#$Rr)AQDU7&5-ku8 zhf5?n!lZ~Asi}q~u`Fa<9m%6~Er6n%G!D6F^g?v5B&!k2W7d)ilI%ShiU?RXDU44; z(e#Po3Mr!)s|KoxqmF79?JT>3qO!`0f}&`paDa!gj-`+Ayfrv!@DFA5Dr_4#75NgjD z{NY&ea+zzcv@sa;J0?kSZInvKSQ6EDQR`?EHu<2GC|xwBVYHqk&f}xSq9vm@WueSN z3iucR6f~faHH48Q)bhrIa-{}d2*#@Q=i{wl2$W&8^CGCrXY~Q$>U*x<2^upQbvJ@L!da^^IOo zK`kkis+uu$qZx(d$|4U_c*U8;e6~Q18Dxs#kq5urjviDs&RZd)s1!955J{3Tx?3oV zH95IECeS1)d3|B+4=x9$XR)o8f>c&A7-@L!qX0)rm@}toDo+Fe(gP zO=0e;><$6+T`8 z3yX#jgFxdXSPSRn3g93#L`9Q0PQ7?&Fz_uVPPl6PXf73ziZF(Rni^f2Jt#UW*?rN` zx%dGZ{c%{ zYCJ!6rS|8u>w}xMu}ux>qZsy-)sZF%!bcr3+CklAkJ&*ImXqU0lx5B0;%|Cl!T~}p z39luaSNrP`gG4*)l;|d1h9?OS%{~e@_=DKzOgMl?>?<9yPm=^g7^(OeY@vfE3AcHu z{moVr(=65t8d+$vxW?)Z^5oPO#hb{&$P%d}$kbzsPCVIQ1adT&4A4 z%8kK*VURowMk&IG4o%g>=R`!QF&@OMEZnXwOS9NE-l8IO!m^BnyfT*LO+Gl%@@A_x zk6EBojC{{1T&RI%-9#>x1FFVUJPpt?3@TAY3}$jX1#&$5JW)-AQ6fg0$V-_HO}U=kb8(z|@32jtjyI#VJSIiw?JD*A;Bux9Q6_p1R@d z*^kbB`~KISxWVtB`OUT09>49i$NuV<*57!1=1a3~c>3OJ{6O=AGq0F=<>X7po;&ol zrT-bc3?71t047umib9=0A_yspNu~zug(7VS$L1^?N(TsyH=S94VJXNJ_*zshQH(5h zY!Lt}7swp=6fY?x)PjL+DKHYyc=3WS@YIA#QG}w|p^qrz0eNyZk-U_{S{UR*xIvQz z6x9)i+p?<&upIi4%7PLh+}P3uV=+J|7;9hH<}$<@P&f9D<7hXG48sDvU@VX;YNr@U zr4}d~Ga9ppG@xSm&e`OXp3p?yhnC#D8X(;@B=n7Hi3#a}ce=8;^4lwFj3yT(k1ok0nUKsJMC zAMPe$IVP!c>9}B8e_H3b$Yb20;#TDr5bIJ))4*#KD3c3g)#~oxQt6K1qDsf*l#Vr3 zT#c;|D8!vn7D-`2YTU|&>@X-)sO%VLQ=OcQmkJ%8#5*MEEm3DFoqZkbEccF~W3D9l z#Ocw2F}F^QqtRf5xJgBu=#*$Q&8VFB!}Z2E)Qk#^5IPjVH#{dL$r7G~w}zQkO5myXQuD$@Fg19B9D6ojd?>6uXL}rhrVvh= z#nJDEsO)i_Q9G@n2$l&APO0IxqCpH2Q-E)Dq76A2qg2$A5{L*7wUSGmp%R@QLq(^= zloTRST^2fqgn~4P>_MW@)GDhKo+uYjho5*QB5H&&dg*IJ+4)ZanbBHCO=(82h4F}z z$PxvqctlCm!fr1JY$X=Z1Ny*-#=CVE#<4sjw=hGP80W^My2Kzf)LmypV~C1MRco4- zh)M~N5=BSp9OI$VZ5;C?+E(3d>kAPHO=@bVYC2A3bqYqafQ0fgOJTTib(E?ZH3cS8 zl}oNY8Ps>Y;vvsXC4`Bon%wTuWrUW^Dis|nR6C?zam~@8E);PsvbJQwMc|L;A-_M( zi^hp8F$QE&V(=`)*kGIxZwSTImPK9#iN?4J1=%HOE;Z7sL)anF0#Rk{BWb9D_S$0S zG_ivZCdbLo=t{W8(%K?CASa}Q^teBs zfn`V;BML0rgO&<7JB(_buPmc0fEb1|zVQfoanCRd2yFc?8fq~oBF);YaZEb=v*o+DdWf%|&4KTyZ^w+9E`XxwfBTb#`wHN?;L^l}*qLSniH3lq= ztbziIB~$fX+?`!2U9wPIAu;aeE~oK*30ErDOhUJQug{!5^!tTHZ|X#sBTn*MQ|k_O zA?LG;p&F@(P^TbCD5^#nqg};ilm%f#=|m&Q5(Z_#Yq6M9rj?;3dt8f@9vhd@7jbdW z)EMJRv&R)fDkgDp=yiVM_d{Zw$FrkQ#ubMY#{rX?OUhLoZ=&h$&jLG? zqi2l=`X;6jkEn$w6oN>CWaNw@7@;3th372LvE}4zNEv(A8AVDo%_$@PkXXFMT3U7k z-oS>GJvh?l2}C@Qm_%CJ_?N1K0xz2^`h_Rqr&=RLD^bym^BEsiNP5K?d}N8%FmA({ za9&c>H#|Q(&}0)i-d_Wa@!&J^ULF{N35iW-U?j1BSDp?s`ygR|Lw3Jke6C0-NL~EF zOSgaa)_rr{xaZBMZ}wy8etG=mM{e=k5Plfqfg2~^f5W8vZ<-SO%kc6QlSWPQdoJhx z&){Xdv4JJQLvYe=FW^ZCDMQV$upL`Kk`U1WEx}l7wshfEc>{9Fi{MSlfm?;_z@Bm? z@iIJ4jj6z-_`tS6E|Q%*aI0L|`*$kqt54r2R3bTatVFn*9Y&%!Za8{PLXD7zj$zAd zZ#oJRhQs76sG73E0!H?n1G`jKiTD8Zk*X}`oS{(^XRRSz&=ns$#ULsS8;7y&7vt^o z0ML~c_U$~IvMuPTux@RTtU%uNDs97px8aFkoZpatB z1%x47SQz+4Hi(g^9c1X31Q53A91>xWBp~ziH4?DePP7m&Dej~^aqCPvAnNwY zC5(Hkw@`l`Y~qrrrq*RHWx4~S&Eqzq-(BURb7OHHSID@lYLmF*Xa_B$owb|x6xFuT zJ9O|!y9u?kej$v$z{rS_8Fhj7q@z z2M$t1+<;=Vn-Ok6LULIUH+V=AjF275l0qI!g{U$IIKwMZ)#MUa!(qI>;FyezCuaH#YIc4m;i_Mu=* zqL?NYbj2Np9Q9Fq}v`GHF@$vCo zND(S73M?xthLA8SA_;@|!$S^7RQn7KIeCBIBE%RuE|M`wbYJuxxn8s6;Ea07$ViMp zwT_BM(ogIxTPDywD;v%q)Jr|Qd%z{@G_DxY=^8;QO+t2J!m=F8OLnxF#z{_OIw%SfMjK*c;!BFs zhqrC$3$?8ZCP}?Cl=jhdDyxMxnsMT(0+RIW$@X|261}Wv|Ad!gf7v2jM?;``T-(a! z$rV#bA0@$#;*#Skx>Xbw4uXXdm^_?-WznP##t<^bqBx-yKCdkP!Cpzm7zhmZ1}+8! z5)gRkK@FY`KB~am$VJWqJIt~WQz?c-G*k=>&0==oC21>lAu(n2OLUDc(84+{#<2D$ zkGlhjPLXOnhrZ6z-BOJKR{&GqAVJCAlu9{qBNzh6u9iuPrA1sF;XLNO7df_g1-X#ostW)RC!aYhUk4JAbSS9gm>(?u5cb*^xD z^J;KihA`3Kak_|d8w+$;u` z<1wZlaWytgl4RJ&F0)y~AZw}tPGt)b=n)_qWX1y~@w#YdrH^?Q+F$lD|K`#i@N!wk%1>Uud-f}LeE9NT zUwQ1hhi|(SJ^$bheq9k{zWK8AZ<#i7=3hqNe&zU?GsfR=*|^Imj=W^_Isa35InXXd z2|0qUa3#P9c#7Ix1uX+C2a1F!MWIMQHDIqOHOhdk>=>z$5m=7hUdoanP(~0&Ui!AG zVOu2zIORiNXZnPoYeD8-;k-}YWhmnIE$u3U!BM6BvC;KW9va<{|I}~B88r3GF ziErKo&Nb{!z_@@DqPWOg&sckti((>_m$K}Zd}fn5Zu&?yYg`jTSt^1oOe+%lo`zAt zlZ(Q10md2W?CWX=l}JVMwm0Mr z`esp|w=fbC%Iu(@kGt*xI3~EYic7A$7%X6Y<#yvxkQ<_98-!v@)T=HwUj5)gwTANG z3z^qi%rwVnKrdzB<2zR_2ng?c5x&{zqY%Ch;VX{56fx*yn|=Z058gK$eZmo8BvGU6 z!x2I{B;+VQThZ6y%a%}fAtr1(-{NY+C8c}yBQ8F!Mf?f(T#;O$JhkHtp|(w2MTZ{x z>B{JZxO8iOeHz;wB)V^?;}VS-6b&0KudR+9v~mNn6Ro6iFdcTZeLQNYkqNc!@k17V z9yljDOB5rEj7;I9Np))rv|@X)12A!Z%mGWU!y6tRfG461Mz=;cg)_2;LDo(UIu`^M zB|46gWD%THS=anHC>1qQLA54VUn3B?$m3&dDR-Qlvn{H(08lY+k z;f9s)Q!_?VwDXYs_v(cG;l;702z{!i?zwJrqR~xZhe4H%nkpBgH}wl)C}Aud zk2HmO6iKq^1^QCS2~Ci=Gseg?eGJHW!Ym0Hx`w5z5I!+b|CocSsg5ilLQ$5ZBUF=d zMX0qT9idzm2_H!K6M4BuDAm9)s2Zy>%uu9I7d@|{cxD)1nTy%#k{Cj7PMJQ0t{$qx zh|-&?Gi%HC8?Mm{BpOH5Wruy5>>J5=Nt(`Ohr{7NBlsBALf$emRIlGRl3YLIy6Q-e zhm!_I^q@X6cnk);HZ*M$Jsw@47n1Fu%Gx2?go27|fhaV})d3{P%MY(9IR)9(h$%*7iR_Uj!ssIw)J{wOda@%q ze&l74Wg$Lry-q=9F6T{H#Q9{sLM{nIsLFE1GFz=9i#~M;LhMAvG3*TX)DLvk_O(^^ zwpIuach{Xb)N2ohgYBY1(c=f9Mb9g2r2gNm-RakDMV0UQ0|<(uC<04^=#)t)@KY&QIgjXgp*UVqNQb*@)JIY}>12+v$vKapNI2K#+ms)x)PlM8U86%8wk=lr; zjYQchh7G+;q7)A#N-4-1K@WY4(W0~TtjA^Zd|BSt=lwI{8Le6Cw-stJ60TY_Y!R9^ z;w+%4)1#thiqxB*NbE4K6^zz(vqMhyx!S==qe!Wkhz1?h(L&uwm9dsN+>~|(XjXeG+jF11BEp`| zUVQa9cDcOz$@_eHdBG#Up8v>=^B%t8-d|tle)-g64mjz^eGmQ6-|hY8ZPBMc^3F3) z__Ti(dGP!9{KP(cd}Qz4KKRbv_I>*s-o3|b-}KsTcHaKK?S2{RgpL72X&1o&W8fE} ztW*(z3I9Tx-OtijR4KIpEFmykwz|?#Fa>4<%K_5bN=q|y@31xX0nr1eGvJ67dZo{*n3u=(G5eJaN-HO;p0dp74kg_5e*-wOFK-sA%cDd{*=vvup*uT5!$9C7@ z+X1kxmv1NkAqbu_hphsjskP?N;s}?-USTIu6r@tr!F$|)TDGlU>&v3EMr5trjIa}M zCQ;f^fezl4bcaLBu>Ao;+5TgT3$c2#dwknM?Z!@<(#|kF(d?*-C=lAxZI5<)yeBec zMUJm+`*@qb!vMx6&#bzc!YHDzMWu=mMwT4mJ)taT$9r)L8khA@>p>>4;;9sRCGiW5 zey}l^32 zVN_%kXkv=9!BVA%S4Co5w3%%nD630f#Rk#nsDfBVx&NS=+d7 z>Z7?$h)g+sM}f-b-c;XMW=iWQzSenFi=NaTUXcaTgRfwk^BQ7P)7FS0gwY(K=Lm)a z`Jf6P$gLM@Dw_G`|D?)*VmdYmT_KVkL&OuK!zD$!%oIC}_oS1>>vfWNMFoU?wU~93 zLfK)^pof=Dc*CmK4Y&SkLMWOK>Wa23e}sq~grQG?5vI&Q&qowYmYS{>PGs$Xr3gb# zxq!2bVXcO0T|kyj7e7nxkWBn^6K%^*Y6%h?x&96K-(8I`y%kwQQ;O>kYEr z!$I&eo{%YB<_t35L_?M}3};A$sIia&)Q))=rGPU}_|S>2naoH&Q&3l_aW~xdKoh3< zfbnSwzA4&(##^KvO~+GWW_jXZYy`!8jF`3rheAZ&fjKo6C|87ke$%4O>-<0N0T=KJ zHLr>uQgm*=(a}u`@bcKGx>Je69Ls-< z;j^Y%wjHPWY?u_itb=w5MXFb74eBFeiXdJ2o;&w_H%d{s5HvX z(r^vFA;ueSkW;GEVLK2cZU#aCrGhEENMJ?Y1T!>+T@&X2uy7_{US76fx*z!R{pH@f z29u%VAhY{rZ)N+jFJE5vhPGDBC6mh3;u81_YH{FCTUMK0bEe#xsF}L-iMWIvn z2V*3Q$4HHcp*Kks>1k^U!y361r7vw=C^f9lJUx#xo;(FD4rV8K+0Y2Sup2nkkeNb; z#=WOmq|{IvwRRY7**x=vSxmu~nocGX5AmF7KGznB>hzKBNGTglypAiH%=Z8K`fa<7 zw4F!%t>sqqIChmbUev5&!0?Zomm1}czN*-qjann!z7GA+9-sN=JwNq<-S_*4 z9ru67PDgxf@6(St2xRtqUkB~?mJhybm-qj}8}{34mwn#)#<%UZ^;j~KWTL&5K2`5v)f_o7Cq7C4E3O~BUGEoEw|D}qcFWdZPzpstFj_pvec0#je| zpU*%vp>}!uyWd=|A=v75>rDfQ&KuQN$MC=0yty4VZ4voI1VfC>yB5y6XW3lU|LbRhyds6X*W~>ouXlRQ$?HRi#9LA* zhEA^z_A(Wr7m#|*DK#%CA=SOfykO=ZIIwErqiYsVmoA_8kSxbEJqk%UhRF3@zjUNc z3?nR!1x8W(q?iaVc_9)d4CP|Nh$1>9!jesl@ui8VH8C(qVCiERpGxiOwK#6b4J(@% zgkf1*6tYI2Tsk(*4O=t?tq>}r$pK?m#X5^Gt1ooal4UC2@FqFuDJNtKRZ2)f6-826 z^N{`+AClUoP$>q{@ibvpbt#>ge8@O8!kSZ85&va1lt0pp6aiVYS7frLF1a9!e;$rL zWQsC5gQ`&GxmN0h)EV9f!d>vuFDq5VzP1o1zBSHHp~hG^NVMs*%zj?sRWu2oUOo%0 zT=0m=xq8uKmWiby9j7Hm5ece;Ebvh1!WV4H5^7F2rS&DA=YmtH`OgzhRz;Q%()z;Z z9Iy~tr&4QqY@GxAL>Mcm^&)y~YKw1LVFYh$vg1{x`-FI#3^|5^4NWawFf-NSyy|0! z3R>ON7n*8O;5OxYM~8&-`d(ep&{L_{lBcW)k!TAQcBM$)9q1e-!&?4uDx_%;Qf{UHUq9omGI0-uyLoQBbj>H&EsH>0cu&FH` zHi9)FtB-lhLrvs{ifSoUMP&2>lVe;ZN0^+~i>8Wz;wh>^78jnh~pkZxTjoiW-oNnYG7=RKrI7AZn%*-YAz*k?+MvYIx~x`0Q`WD1{vs3C2s2$mu^7q(Rdx1!zyr$eG45*GX$DOtwZR7G`izDc2DYKWWz zeVLj9*b57@Hr_R>?)7X!J?f{)bb%VGU$Vcs;TrF3lL&%na7+`bD1uVm2>WNvUd)D< z@$fH{>UJ2Ib^DAU+yR+hdUhDL_pa%<=M4)(zHqMUe)Wq5IF}JZ47m$pP&dn7s8)3U zT{o*sfiTqU>X|0g>?RsW$JCZmJZGTeFCG8clfQ5{Q~XgKWCo=#zu#V%?I(wPKouN-Mmu=R zBF`_N0EKY-J`o@+3;Ft{kMG$L;(f&iBE~DifYQEb0}lJ14!&;*VHEie(EyMqUo(WV zsS)zW@jkHJLlZ*rY$friMHtzQtDfAvX6`fVhOfGyqSt;XE8s${zwNq0K|;qUD@`u! z=}Yl+CoQHzxGM2qBv zJmjjZ>MCMLFnlm%lxvrq=Z4!RKN_hm${fgB3e;H04oz7_n!r>MPr^90BuzP4&6$Uu z7nEy}CZUEY%f;H;(PGXXNM;oU>q^16k(Xsgi@B(qMXm#0y)jVK237Q zGZm3D-ue}@$uY<*UAyFQ^VS4bR}*aNpsw}ImE~tmOLH5er}XodO9G-JD(02GApX=Z zMv%o~RZjlx9VdMDn>mGXfnK7E?=*zd>@2B~X`+C6Ms7_A7SUD5vz=r>i`;zYni z_5Bf=#a~v$+K~iDU0DtY(GZr&Pk31-6m)@d>!m)XELD{M;;AnJLV++MWRQX`vpID> z&=d-h0b8_4Qxz8QW*0dLeWXVmW}$V85p%VSd#)^54eD|Q~I8qveG&$<%}?j7$If6(KnO~YMPMShQEAUm8-)pQabsqZQRl3k(F4~CC!Xm=`cfWu-^ zWWgU#f_oNJBI`wlPbt zISP2=Zsv%yAR8TnCBp3(QXdWd`?G6F`~bV3Yx9Ft2F@36#YA-m`+1zAYRQX?rZp#? zN=2*?(HGt#OrkC##B-P%{8tgrh>ECZUz$o~mC|YD@?S4VP)*RRX%dz-Es}}|i$iTG zvd{vgQhduqXH8eB>8HOzX$qE-PGyiyJUfCZ_*Nok8iLUiebV$mO3u6HS1IW*qLT1jA>3}oH3?|FspY@76pUr%$U~&Qd{OTvYj}0&P``FX( z`pKQwUv$nX2Yu%opZofW2mZ@P_I&@_U;Xia+Wq)XA8_&!2OjmweZO?XhYvg8ZTtNF ztM}exNB7I{^1JtZ{T{E|{=W+^gM?rsoY?I!lnPfOfD}QFP;($RP1qAerB9P&yz_;Q zNyz=DV;~p&BvFw*BTUf*dtqCEmx3E)Sr#yqNhm<9DN{o%xYve)VP5j|fqHVNmcz^v zLdSFfWbm0mjZ)4%ydp51WvM0@REt6F%Bsa@47J|bCJ_(_T6XB>_s(~f41Pkpf4Jj@ z-{1BNh&8Yb*mn5~zG)dNf?58*#{Em@dRNW2LI!J$_k|P{)j}AheT~sO zoL*J(UQjO%^~RT1p1eKf9U;PAVM>9d7b(3#)XQwLUOiF^VJMJ$-H1(1ivU*<2l8L) zeLSuTORodzV`_qLqqBY4$Lw;lZP$eD{DwySkwF|b>ZSf?pZ`SQi!i=t!R?etj|M^Jm^GAl`@69G?+8QNc8O z3*h*S6Jb2Lcygn*H1{%MM3LoCuNR4lH;j06Y%3iVuS;%j{ST$<;*)fnFu`eZ&DxyBFkSZh>oslMXDkC+(3BUR}o)uR2^TbZ8sMCnl zL^VU`=t4_?0~+Fbe?d75B!XIZv7H{6w+s(Ufjae9cqR^T~6vt9ppR(wKzYHLdrVM zCJWXS1!0*Q0d`o+AXlTjZJA4rPFtw;WoyM7te$5LB;C}mPXmxfb(u>AB>W{N;wDf^ixLl2(RUP@hK{GsPil0UpJHN4AdG3CFM45O z2t`va|Hmm|tc`vr79(zm@y6ZskhSYN$rxhOgQnD?HXleZlw0G9NYYnV6>=7a88tc2 zw7D598&SKZ=-IVIo2$7bm~Chn?x9wVP}6B=%kApwTJt;$QW7>bF>l5_vYM7!EEbMv zXp3dzNR6Fp5gJVjeAAHX82*SD;~HvOHj6~qv@uvFlF?Cw@qtXAW*7paraFV_j&YX7 z=bI+*e5jO%MSy$!G(dHn5YI@S<4IV+z?|PfbJq{P`z`N#`)+&fvGXpk+3q#lO&k8{^0aCG|MIrO ze_k&4U;kVA{|^TmgO)%jf)j(KKqySw6)glR3uD5kgn%a=kd%U%2z-WiNu+QnYoIO& z%reyJH@mv23;XmM8U6HBcK(? zB`*u)!mw~F35*QYx};WJU63{lkTNh1*s92^EOabo0U}m~ag6kq>#sy^nPsO^Ewazx z@ZXlddIE#Jj}5JV^~=Xxamm-t`qGi`^0nXprvD`6$Fbq!P_yso`9`z1vV9T8``8#0 z)pA3Yo*CY5FKgjgr1pf7g3{w@oK8;xivrV@(&3*SWEmn7c03W-2`{^~LGES7|5rr; zB&UE;WdF1Y+jtS)rq>?c&TRke5uk{I{oE$meqLIw_IA4^q3zc;i}0$e*mHp$Hrr_x zVcL_Ofx%>_WzP{j9j)NU2M|qlZP)ZQfikF31nT;tmeiMPpxwUp10Vyucz9V+I(Xmr z@l~Cv6q*hugU`N(15+bk+sQ&sa)8fu6QkEoh8*+6l%*r(Ihwkc+H&oL#?H7%qc*q3 z$SCLwJEA03y2g}N>SjHqfT6>9%@v{oYdDnRrF@_!{_&g?PgUsiKe?2Swi=q8&jNYY zWHnm5>8mcwHwxS%B&jXixY=kv>p1FJKxmWqxbZ2j6T2P0hmlfsQLZwAa)$BxrPNq! zh{S_i45O(J7f~hDO2;IR4~#4!wXsUs9yb)!YRMP~Mcngz5jq{wv{)fQL5>Fkytg*Y_fpZY>YJFoTE`y)YVX(QksJ5<5;)8(y3jh41wy|%xy(<2r&o~C1GZ3#aESn&kw!U2QKiyR z&S3^Mln}2-j~u`p!yF48y&S`WAx>iB!3^&_=IH1!=EP{ZSq}bl=)HITLa$A7b6yaN zgDzuCeqC;^YC@O$*?q+W1ar}Rzn2qpSL5dn0!E9Jv8T*E^>*Fq(gUr6U3?SnXhQj!(U)&f@ z^^^ZuCe=Z$3x%eg{QtOVWgir9hLFUS8FcKGY)rADuN2Q`yzU@OJuMpC=BoL_PgFEy zTSp?pc{DJSPhxw%m;eUXl#sQ-n%Hf{JzmL81{1^dK+PgqXvpj9idz0D%Qi90$%cbJ zVr$?uifOKLjC-SF7$*~sHIxNeIxYwLEVP=~q2D&82_0t>>AUeFQJUpOO-0)yH3sn% z5Jr~1Mn}tkF^&7GT>BaA#As=!_Z(##oTF(~b+%Fd5TEmX2&yoEg;WIAP#C zcJYIeNl%+&I)$m@ZI8`2y)HXUxtE|QYvzoGNX!AVB1DgYI;Gs>i6e23hS96$i7HJ( za*|5fv}2g+8sf${5ZvH{QOFZTMgbWV#+l*?BW5WdG@TE*fzPpGHM-GErc+d_9^b^) zFFNCU*RY2BWng*hx_Qq#fI%6KJ+90qP};A4c^`Xt-}RB(=RWY8NAJ7_WS)KBb^fK) zj6YoMm%c7N>xfIv{Oq}>9(wB0AN~9x?>p*K`y6}7{$D)&qbGm%Lq{I??tgyon?CUN zUEDA4_x9c0FTZWKopycg_OIJvyZ?Q>+yyHj2_6ENP$azRD)w9FpXIt1v?N3n;8Vgd zrfe`-3TaAR_yVN9HB6a=1(so9SXCC3#;D7<`{m?mg3mBA0LuZIu6^NareuSzL3k3B zZve#z+o$YF4I&HXqSAvKMv z&W?%*H0G5wo*Qbh<6UkD9peRWFJsGo{j}rGKjXMlK7a5@pZf&J?7uKybNRWy`sugt zyzxhnaz79OCS#lO_qm_8N6CL|1s|IMCvzHhU{dd8s%IfF@iSRig+b!_`E{k zGYx?%r0u$ePB87UJqV$cGR!z_$Z*;1d z4@^1#Xo!$YJ&Z3~l@HGP0{v6M352)dUB0hf7kt zPW6aaM1f_b6;D0m(wN4KYe5*bSe$#N^eDvl*fpknkflSJ5TiO$(X^p}G^bxu&IlAw z^t9L#@{LeB>ZqjqzNvz<+*Qd7!Wb)|}EHe_^AO=Q1z z4S`-bQ$~dBUN{BY~_G4@htO}0*OEwYs3vjLysm{cwD(WG*U*5+lbGKI!UIZvyRq!XH1PO z1r`*kB@{)VEmw7#PcNUhe#QJ~!^#CyY4xJnD;Lb9uxa%o@{Ow&KC^Bq;ku=B=#x}S zDLJ*ZOP*A^X7Ll`Th=al_NishJ-uAlJjF~=9n@1D7Z5?vwZc_2^tTO%!Z^)TDS9G9 zSY#*^G}Ybkj`1I7kt&GJNZLilYoHZ#EHqCs7s}6VT<%WU7GZ@BkIq%|!poj$o?jND zB4&|ZXt(vHMKGC5b*HJKEpqKr(-*GlavjkqA)>DqQwWos8v6nwYxK#Ps;(-8jA)mH zSrQ*CH~L1?f*p3$$&@hAveG6tIlfIeIf-#^&^?2wHGisThEN_R+uDVCjXcl=L>z2c zf@6ac7J-s2IO0RjHxBD~V_K;7LZ_s9L_u2j$I@b@ii$>=veB&Xti{xEMIuS}TqNtVJY-yFa3gqm=s%~ zFhF7`AVMT1F>uXx#~7y6EheI52ScG|*&-kmMXY&CnY?fDkaIPki(XyB551V+VySy% z7s_syy_*d(!_i*A?$TL`CxM#R%(>tD*vn?#v0?Fp>lWO#WyOO}FZ%rtFZ{x>hrQ># z(+|Dm%p*@a;(){dd9ULR`+)bck2~~$;||$(_zLrT-?Y!3uYJevJG}Mv+wZ>9v|V4l z&F(wD@(nxg@T$obv-{<3Uc6iep#QeNe(xH11Qj7ia1vSsO94hg0Je)-yc=Y~h-M%Z zE>6dL&UCze4TJ`?p;vXGPF2{X3FNZxHW^{)v(x1-1r9Jp;(tH+0WRnm1U~ocr@+PR z$Vw^D%r5wQPDN@C~IUPVAfys)P0^T80 zcens7#0tHRc$UPg)Ih6yXJ2gwcmY+AmIT688cYVUq3N4`euc|rJbcX{{0lp0K?+$@ zW(UbCl|tM#*9+UAw7Rf1XEfnT8s!_jOybV^oUfnca#+%I4F_2XSJ zpL_Z-{ui_BW*5!A&fMGA-gy9%{X(p}W-n&@c*op+abizCDYYuwD=a;Xg+LO-v$F8g z@o+vIZzc~5%Fti<7f-FHgHm;2LohH2Xxy)9DYBZZbjqC=foQ1Nc8aX!ag(w z0#Zs@gge#u#Ka@*_H&~=3DkR%@`r~Qo8dSj>~M@J676awzBa#gS%NTZQhS{i(?a7c!am6pu1l)!oRxm4@=i#J^v&$pbx^F&(_ z9UdZb{veWg7DNVN$O%nLBVvqNeN7cpv^ijsG*6lyrcib?msyOibr8}`W_1%0HVR3k zO0_FhQFlmC!5QPDV(u~6I%1?!jFT!V>*P7|8A-DsOY;*$pUn(P^~<{Jb1G}F!&Q^h z%tz*}IZRG-*eWLrOX(g`*j3UOtedxGgJLn%McKbU)G~vh(IkCl&VE2I0&N( ziPpu6O~wg_wx+x&!o$Xk+bvk7Jm-JQaLi&TDAHvIOSKH81t=6f^6UU6fLg#WdTQBR zC++ZWkQu*f;j9(&XM~O^ppNAozvC%8hr`A6DS*uEBzb~SH&Y%5&?ieG>Qy1{{Q5^7k;Z%e4qRB6e z~fjVE%U`^op{+dPXU(CKIL=pvY*TT!9`#7 z^V`0^><7O5V7u=Kp#G7H7qjj8)`h8Vp5yHpC}C?vsjVJ5uv#q^=RG`$Y|0qag{eiY z$dZpJSnL(Ci7fnZA~lU#R#FXFVy!sqeIzTo{onFO?X*3=!e|b+9tU6!#?!%usR{)= z!k{(~sg2#y*G7>w3O)VRVj0;u%4(zK-vfQPuE9M2$mo1vH(l3)hpi5?vto5n* z(%Bet!Zq;n@+TE}DacDrUQCj%oqQK~<>bX?5^obNoqR9P+n!H829Mw8EhF;YYm%zs zRU$$!9VI73EU4?{&%wgN$2GUkUD%2_d>b8G4AeXPyHj=v4jrtVi@0PUgx$a)Lm5M2 z$7_GX8+!X>b3gziffAw~Mjk+7B3m#Uh!jW!$|#7K)X^fIoPr`jMi^$rmYimH_Y4{f zLYg8^fkdh~5{&paG<2exW~8xe$ZY#c6-iA_AILm-_l-W0(05a-UN_Tpw&{vdq}Lqd zvx#8}A-=K94e}o1Jj9T6o-9yJS}7tidpq!SUXo>8sB47|NrcZIjIcK-DJ0Pg-O;ZC zSg-LIE{QGo1cxM-oCiF<^^O}FWpgAwwH_^4ZfKp(@MxQYf3*^|B6O2k#D~TuNmUB3 zln@anr^$#^cJOe~46DGzGyR@&>a2!F(sxC| zLb=fq0kyb6Ld^&^KS}b3OKJ^8UIPmBu&PaSCG#qwCV>Hf8yrGvxm?oG>(iW-; zYw9AFrMlMnv!04T6GlTy3z{NwPD%;uQKi(BN@%qs>*;|*nf&v`zNZu@{s@91a_>4h|C zkMCg5t42_D2Xe}+5z01Lg+dn)KI44g39nG2GmepA;xM;c^%yHFGH~=;;})IsBpumC zmZb3=vuhU5S-S+zox5(S!?r6bx5n{BazKA?|!s#9hu!VB`$7)~Ac=3Dyd2v;OHDvoi(h%n8f z_p$*ZOfH6WSZHW`Yz6DNJOzziE@MDK(2pDp3@-;2r9Mw{&NmQ-nCDD!i7A%4UTp{?5tk(xpXV+*oFZuIlue(}t!>8s~^(A!blSMlVV zo?b$yP0S3}YZ1_?9B%E%@>w+OS_V$g=oHC%MS+zWON)#F-DM3Dc&fnf^XBVuIdJnn(EIzz0z{CDy>JW z&-#t5+|`SoB(X3Rjcc<_gUvo^(P%9)7^4WUiDm`fnb}WGIz>0rIK#d6oT`wh#nr*R z$>X3viHWgj-5IQ++5j0TVN|3RV&`RKKI?Mv$v96mN+Qd*qKZ*nD3lfu&g4HiKjCH9 z$^f!!Wsn&<28!J^qv5vBhXa$FWj_mse2dxr^0FCs!ONSLKI~=eIS>AF&V$$e;DX~$ z{OmsGf90^Ro$#>(-?!_B-o5jQhkxjd;|@LgQ~SAJKI#+yc*p_&aKJlu-D|h)|8A#k z{Qu=Q@4SQiiw_pE57s`F7-q!@YlI@DwT{C=PU9`hnBaU;<*oaUz za=dA)w<73U>3@xvEo!%)Du$*hL@gq#%zA8PS#H)H%NPsX`cf*TsTDh<%@f%ME$Y^F zJES6;p@)}!81)pR;E4c7U~C8~Izjn&678Z=b=yl-QPgA9e!`lqp1z8B5)4Y2nzjf% zsvNLK6yShO(rLe^7(SpHR`op^P#5Oy`#zv<-?$0$;^ALk3j&#?bbSBFuWcYuTGlsi zpk`{mn*(mMBa76+`%2EjxsN#Zdy;|xT3q0Qd%^U)@-PiVb8O^oStB*drS>%P#t75s z7?CmTA|pOXaD-)K#AJLXiY;DBN5l*S8i%Ipvf*g)WQSQ2=NXT3wbVr#wa?dLL#Bv6 z-f(CUVXEF7P+g08E2WPxCZ`ws{ov>DUHF$?n>4k?nT$6NO}u8ND%xd}w^Dp_Kt~km zopyOh$ZhjVteUGmUxtXibekZgh;c${R!x&!cBJvCq!v5W@~5R?Xt<$W%@sY&$QQ=C z$xmK+-qm`hM&C52b%WcdTX+(_S$9+W*icRlFXLHGsEG!&;1pyjBRSdVG&JPYEEXT; zIMnr#j#+BDnxdQ6FLlN6RyaEeS+M<(NFK4_Pn!NTOiNYfGFRBF7asUowwaj4DLQ_Pa#DO#W4zZYwH*Hv89Jo+K zQgzm{kbQO>{LDkW)@8jj2>}FIeY6%VLlP-o5QgP2*|8a695I_sP3O5RxoN{UP5ZD^ zv_ddX|a3-VNJY@4PTb~L7)U`&R+te&KE3rja z>r&3-BtGYjII^_K`K&woP7O5^ii|W(71?j>q(-?B_lYI!FihRa2e8fLctbquH~Qol zqwmpR)2@`n)RM~DpzKRFD4S#-^z}?996#<$PGV=%aYG=&{O6~%XzNs->l9&K#t&VX ztf>M*EmDhI9Hd!O3sEzg4JgfKg@y#*(8!8aD@%OH>10{mMvXP2#w=Gc(i~8?xl~py zlCgCyGNo>{@b!)kUR#RrN~2mg`ux#Z)|f)wFN1Y0H!(Lrt6d;}qF6=&`!}owa7(@8&;p=!>8Iu>Zg8ee5qB zx*xoJ@cwVxf3G)sAA8T&ZMVnkwuP79xYH|kdG#xIdgUv2nD$@y%iC_V!{3IMr%eM1 zfla9kS?@L@s1wEvNRC(%{DgMN!@T6+B%m5}1$cccnFSIGu6=2Os2L}5!wdXUrs+G* zu(pOm(_Q{*4rmR)-u>H~RPoQGSfdHODxwC%hI~0pfuAg>Qa1EWA8B3FngIXtZoQe} zFeDBa=bO5WFsO)qMtGqungC#sYsZtktyActyZ69NlZ! zj6>5rbhG@!i_h>rw*M;SqS@uLTW0@`*|(VehbfTRFMatK#}swl3_jbptyvQuSqrir z^vG0~dXL(ocA~Qos0E^>Bos&x2C3*=)Q4s&hN($dz>j9L09G^DvchDr(_`Pj@?Kdl zs+IQ5EbpiJK9^V2r1*5a<$^(de?;lxCmvbyj6TwdC{&D3z4se5n zZ_AylZIb?3#>C-x@09E7x&h{UHji>*+Im~i1KOnv>2 zkfz8lo9iD2`9DFk%2q{|P?3-pbMi=5s8!b}gEZ;jy~rdTOH+_i&ZThz5TeW~RZFML zE{_=f>iMGY$P{hT+Qxe>)a5Lwi0YcYBQ|AoL+XVqk_9s#^mc?NR}i3hGTR~&Rgwr# zZerJNt3xW=lSs6qo;5i$RGB|RL%h;cj8G$8tMBA|!wV?|Us2SZse`L^W=ZNSH*x?& z(~8Sfovnj-X7M$lQIgsvY)l2b(X6^OCn-d#L>7pdX%a4iVZ8|I&3P|T&wFg^meuAY zISIzpZQlBlmo8hCy2FBgEt>8BG7qnzqDH6Yv`7c9MRK!+OErqIa+l6|3~^ZXyrpJc zXp|4Fk+Q8~Y3qn^{*w<|!cWY&Z~lx2)Xfd4?92y$Z`|>Mz_KQIt2xdX3xUFD)V8CD z0>>h*GNn5RBTH_8jJV;(v|(a8br>Vmq;1p;8RL4TLSfBb2k%?EE85_WnN1Ue)<%QZr>&JhUA~Oto`H0 zRYE87hDPwm+0s{i^@tJr>guWBVCd7#n(Az7aa^a?6yoGa9VU&Pz7S$MYC=ys(g+fY zfPc2q`PSg7rOUE(WSycTC+!I4MStA5h8%*HI+K&&DL8m;@TFhK{#xT0ufCo5ZWyXn1FP0=2Zo2d&E@ zQ!ZR!2J!)Q{x|o?xr)hi-2tFrXPR`#*-_Nwl<#AQ2hiBIh>eN$P@8Go}r_ zK{VnGCC0ESiYS;ZE$1XHdE0V3vtyMR(#&b9m}zR+GwoE!lO5T(deBS99SqYhD&<(j zy|EXZuV0*R2D#5Ov{D7pSpHbbN=SRF?3jztbR#evniAC2yGeXD4$PR^J z3QgYfHAY}sqhNIK21RP%ka1Lm3GWm*!RuF5l+A6D5L*`Ow)MV3&$AsCUQ8SHZ(`Ixm&t;WzugHtA z*^WfGuuIK+>&QT~Mk*bziJRE9*pwdm!R+MiYs#aCoTR}2x)2jz#B$XO*skXwj_`7~ z$>C*}&PrW3hnMM~rx!fz_r7M`^UJ3f-oNm%Up;)sj~~9{hu=N-=yOj!_$$YK_}Ih# z(U+IezkcGqE|>j3sS`iF|K~ow_b2v!i{Hn7`>wBWzx>7>r@i5o({_Hzl=jpj0|gDrI)MuGJ7_RowjIDP$+i%LfJ@`onKw?S9J(ybXO*W;10_*UQ+jFmJzk z=BXGG7DB|@f}Lq9^$lqLxHxv{tdx5!E7ChU+)#x2aDaterJzW6w0Pg|e|76kKY?Mv zUavqyqo6U+`n6M!1yS7sz z!{D$pjLeR#>WWyH%GJ|KIaiX93Vw@(gyoT)^>@OYtslz|M5B{59 z`8@3GSHD~_KXChXzQycX*#|y8_AyaW5BgSw`7dkbT9RhI<9)Zr4m|c5uoqZRgr{TS zMAk(_PBX|HUS@&Bl4qY~`goQD)N0`~l8!albG!|F+|1hs?cqv0wRV6{job!n!?eRY z$J)$otu&(!OOCR0r{Cx3`@|!Ez-L*imMxkc8?1^By{y@38SHuC+2p+4k=x^`lePyL z`FbCRIxDW44-N#DJ>6hr*w&4(n_|3|sNEn#$K4b|zJ3Z)>5@5K{e2ii$5pZvI)<8Q zg4n>alnx<%zX!`E$pED%m|a7d{NbAe|H8SC5(Y{5w$T&HKOeDezO>A}UYSQY)(@dfw3u2BkCGBCy zU}M+QoB}x`XlxhXfb!EYag}db!wgT-$%~yjYc4sQn^$$*hHtWYN}7}6K#hWFnepZi zn@FRGal|)nv+udANFRMO+A1);8Sw~1Jr~ABLE?Eak|&*yj8qxFuwX@~+mhiMuQ($m z92VN5*;?0^Qtcw1h>6~n<+D$IM4L;x;OWLfo^ZnoMeLI#m(_6+Ua^cAl-5~&7v6?L z>+z}aLH9=?{pzaDhsKc(Qe-R!4>zycux_za-`v>`nwHOOTwxxXqMO$*1CYT|Ncm}R z4^GJP`OoJ!Xj&`vQS0Ua%d!kcd8-wg6e{9ClZyZ3YMF#20o$<`e6Sp>3uMXQTRWWL zBxlIE!DpItW<9`^zBHe;$PUO1FPrCeQxmfvzS}m!$^Wr+y_J&xs_>y$snx2%gqJZw zM`-#PBV%Di%pxOhQ2G&R695@9H1aC4-(WRTh21gIM-8ph#sx3_MTsUxppa5oRM(20 zz*bs>)kO)%wp7%XP8l86D5&DNH6Df?#-Jw>*E$`>0x41lvyQ~po!#ChgVAg0|M};q znwA!zj!TP%={hXZm(Kq&e28){-$r2}RpfLnAbDtwz{}3njZ|aTbfvEgMMGx`mk1+r zsh-kQ#9AkFUZEadRx4K<8f&V>Yvn4XW2>>@kn5dJkvGuQwZT#tY0P#J>^KbwJ6CUB zGu&07|9E!2B3UW$tB8;er2OYCPyOY&4HWPcU}R8wIEqi+=mwlwQxs&@FXL|a&>e9T zBGX1s^_@_xWD%wv#=sd?pvD2)2!<;Y0zSt2Pg_6M;cy)kMkH%+jjZ9uwDB5co4kFPq!}V5H0^Es zR<}VnAEauLTk^<!f%G02@v| zH=4$`HETP<$QlJ{!PghUs4ZfuTn9}OMBiw(E(D=TVu%X}#xh}~hzXRvk-jViTY>rG z8^`~_$;t0@nJ1w(<6@+Yh_NsV$S`q3yG_3=$m$DT6H58qN-X|=dG2YW)4ZaCG|h(? z`rK261^SANg|MRrMUbsuOVSXKbn6zsk z9ZP-eMw#2D4aZbrhcnsX;fplowu?bJG&9WG{LAx7{z# zyyx2KcU|dyZ2x@ugu~x=%pvbN;WO_)_RxI}`_Nv8?7x@)Cw2PKA9Kar``GV(^Nw$O z!w$Q>W?Oi9r|qY`cKdC2e8o2XKdFB2>lNF*>Tku%KfL_gmtS_tkA863Pp|snkAL{x zZ(V#I`oZ_U_5JT&dgT@0yY8ARfAO=cfA-TK|K!RaAUaq5=yJ-^-`srt%{Tu1!t>5P z_w29z>W1rN)w=bT8`Qe&TNj>p&Y8FU`osOnyzc$WpInKqx#IhmUvlBa z=lt8Z&p%huh2Qw9yI?>VphjI@!`(nMz}susZk&N)I2s~`tbt{6&{$R#2tBw>;)4Qe z7(!;fo((elD8N^$d(9inXS|OE!q?zG2Ci4+DqO9zPXD5;y3BGzTNp(wlT%g&md7*2 zNsW&0kdsq}->G?ZTy@IuJ9Lj2SHz$$aIDW9bnWb~o=nqyy03emch+e>YVbR0(m{UF zS!bR0g=3HS#F2;Xf5OopKkF->z3ALyzjeV0KfCH1zrFdp58Qq2Blq4o^Wj@zG0zTb z$7@lZS$?RhUz6}S^Tw1P-yrei!bJFVq!sy~*vnMvQt*2ns;ej~?T4pSnfkL8vUJj9 zfsT}Nlr;~^)k}P3%#+1l@zw@ z96Vg2VrpqBUANqqEWjoI8*cG}*)|-=-C|$-)QSZvE}QQqb9X(SaX<%L|Dup&(474C zh}2Ww1HHZrS}&6M{SDohiknZ%Dg5c#HF`~`3LU-HL9|%BBts!3vBA_oW~zvdGoquh=eAc!dPGl#b=};&XZs7)LA2lN0=@Sih#(7 z4{C_n&obwcCS(Z_-ass!=S1rJfP+h%IobiF67DiMnHp%$8NXc_PccDzdsjKL_-sp^?>!Qbb63 zYDnoTO*wtpn&1yjRO@u2Tq4J)LLbkptR^^C&%4q0*OG;IJs!G!&^aaP*U@G6(^H0j8)pj7L!ImQ{5^PwgfFKDtVs%05c#&FDfG%Fa-H>UKB%5 zHvi@MjV1$AQlv`#B0*YIg+czX%wZC0c8PprAK|r9ucaiM`SY_+tCCq3s+&Qr+hN*@ z=`gO8j<&LvL4N+n)(yI!GkMZhPmMmpAVqnOFW?1p;S-P2;Sz^YzA-*B*`b^d>|}}& z{_~1lZ0YlXsk~LI{wCKnqCg*EbPzG6u3ADZ{v1#-Qj~E9wMC7NirHseb;hX)AO>NK zVDpwH2~)MGM;s{BcnoWr%PX3!$&#}?)pPclLTVufNjO~4#J@o#%t>-;vW3McH|vxt z%59D2Se{Up#gFagLsOld6flfP$th!ULqI6-d~{OvdP+ivaSVM+n=^QX$>3BE>w=G@ z`q2PqZwFW8H4h(Qm34%3enZZ#B#NADF}?dinS$yLz2gC%4x-a>7ku`i5CrWL2AJ9> zOFk&}IRMlT`}?p+Y1kk19?5>JETRS<~TV_w4-$xh%7)z|^7dekYuJ zTmqt@W>@@_HTS)X|LyFroN)Z-4m$eq51)M8L1&+FG z@baujZgY?6lRzKXn&*MOa33Bd?Dr~3z#mx@)E(v2EF$a0qQD21TWhcqNw~?XVILwU z8A&#wwWdmoZd{Vqz75S9Z?kG|k+tKI+Z=2lblO$ydw3G1(kjxiN1?Lnwj*VeKBmow z@v(?0ysb$o9ouOQ)ddc>3Fwy~6g0e!_xrkd{n8__T3=-AkzNZUeG*iVivR@#BL|9C zxNq`BKlg(^a9;3mco|Gy=UX9OuXU~O?VaJ(py4sZ|D2TSX|5I@Y2DWsYV}ChlGBgU zsmtxL8Kf_^DoT}(VJzTE2}c@YHTf;>2C;z_1VQuBfrv?)A@!XCJB)Bfz#G1Nu#+pw zH~x$d;{1{FL~4^qGbWVAkpvO$?!;+RX@_oZGTG$UW6%b4IOJN_F$6qRgtEur5g1QI!5v zPe>GvYAjbuh-ut0h7kmgsZyNv zsOp>{pL#0GAGKaa!JK%#Xg+bk>MJR^!o z(&QmhQFxgmF9M8YO_mQRxs;kx!~&fNpFyQ*s=`}sp-STkn;4jy$YB*x^C1ZZ6}hBh zjj~)+N$9}B8{D0(UjYsS%aHO4?*J^F^US8zPp@49GQ-ON_10(BkqnS|@~IS44H@Gc zV5*dm@xkn5`w|xVvItWQ^H5q{MXDD=sp=yoEdHuDj-x@@`bEtEQDY~eEH!dMIv5C= zSzeW;AWJ!~WRWfyS&q?HB-@1HCuJ6p>f~)2>XO%Y6^#f@O_0Q}L!TU}g~*XEYejX` z0F@=zq6uc%lG9JKSdp+JC1e?X(U5jILxL3L{3y>2@+coldG1pJMpVi3BEuR5-DiY* zIV>d~drec3rfp@a(`P}J^BEtTQ;|Z}q<9FCWswXoGnEfYxnvX?N19Xn&W;u-AVut} z7-e?!lMgQ=z7;p^vM+Qj(_sXoh|j8zy+~PBL#h;qoT)7pg+hO`K$A5JJRJ4oc}oY? z1fA@#tcp^d#cN$(_(sA#!bnz&s;Eu@Pm_DKK6NKmU zFLXxdnIb3QOx>p+UMA--Q{;KhU_O(m7`*MMHXhr6Xy;g{85VYgbEHR|)iFxpWhsf{ zpHy`RUA)5{1$H_R4wA{cH}0K#zrxKm)a*SRsJXlT@G{68ZCL4bzQ^HZH22Y8ub6xH z%DH!5_RZtGkL~-*=bd)Y*H8G!*S@g->BsDQ#DTl-`_^fn`RJSA<~N&4u-yx7-AHfA8W8+#_FiDSw+P?kfpS$O!OybVh3*>aPq85E_K)5ukDCK#0ISEY?{)6El zvrg$kc$sgi^Wht3e95)4A}E_Cc<$O=$|d*9=bZ7ylRkgg@kf39)DsUo_spX%J^%Rc zT=Yd(%-3Cg&fT|P`Plt_3h54b*)LuA{VP})iZRcd;if%kLkRVl?eJZYFjR;q4DL~A z5s?H2p+6F)szpeXDOpVTQ7h5Zm8ED@7Q>7!fdT*ows~Lg{aQzk?!Yqp97GOJ)*4dN$A7~-582O7or4> z!^>c@D`ub=Mh+TF!DLw!vtf8+XE+n;8V}p{`R`1N$NY7_bRkkiftuiS6enRvm&t`i z)|N{FQ+TULs6`T4MKniJQG5(~GeKvOIE)*wK2i{YaXTH2XsGzt5j#BNh8Qthb9qJ1 z=O9VGsSEp5QD2mjvrMi>97bcm$O%WEvq@4UJyv=cXOtbA^SBd%>k__RZ(>c{CCjG`HWXxs1aqxtE*k4U258@ITlz;N9*7fqCldquHjL= zpve^BR5_t6x$0so9hDW;A|WGu<|@7x74d-siZDv0cye7}L=ip(SwdcwYLQ&udCMTV zlyV)Zic~|%F&U(wu54}5A*X{dEGQz(pPb1Dats~9DCOMFT1}8g)Qn5e)d{nanm5yz z$fiTbJb{-zL<|#`fMq(YshgAaMOG=ER|rqPXloQ{RJP$(Dn5ivX#%y~F~WaEG1LsM zS{cWqW{VA2QE+%uiyW`$Wd`Xq#NdurxnaZy-o0*BqhYFv_x7pp5dLCrYbT>vXJWXL}6NoVs+12`3v1)#>Bw zPS)@>$2~=aOsR<2K}r!LD6?bJN>xX-#ltGbOm%WJm@`_zu#;s?BNA#P+>qv^&JN1H z$mwlW|3GTpQn$(QGQ#}%j~fZSgN}!kU)Z`{I)Lj*0NGL9h_JvKugImMO3wLO6tUV> zilKliO+l6gX-3pVj5v^sRVRFsDzcy!Dmbz+ET~e`8B~Rk1&|P5e>sCmndkH=vw)bA z)hVMOOGhdzjJd6pGxfKksSe`{i%_e}!&+ehvB0d9TD5YL4x&(xC}hN5(Qc-7ZB<1n zwQ+jr6lWCKDBAxOHO+KW$601gmH+9!q=*h;!B(s}Zm7a+4(D5qk|1h`wY(|_g^QZ3 zwoFSB3V6=Qs;Dgrk>=dF^RV%yMuL~(>4=PUG{pjmQfXao)Y!yG`BQ9*q^*@FNRg&_ zl1rRnM8qs)y=F}lh-TfX6}76O7DjfohS5Z=;~B3yvm7Sjp3>$OO$McU%1)$8wNzvo zse)>h5&Aiq5p_w5hKe+kux7j(G7{ZKg*qeL5K^-up9v97W~D`vL@i;_x<;ki8k?w7 zBzcmbc#MiHh#_7_7*SV|g+`xmPHvsnKw}U%!o$l5E{-7&Q!7I8o%)>jkQ1*nUdKO2 zK9UfG7rcSx-d%@?y|eA2+4pW%PrlSS6)fW^uUh!fQ_H5WoOkbvx%VuZdHech54&GJ z`;{Ps?Y_q@uXyt=uNv_3HvaiCf|sMe?Tu{U*Q?R+a(Be|Zj*uKP%tzN zG83jL1&R^0>s@Qe7n%lm;b`~E?wUbhm(lDfCE=mg^@UB1QlL-52+~3Nbf92hoN=WvK7A|b`_neg%{#~=R1|A({t@ZP(s@;x3wbZjGv1r?=?qavf@=!^vm7Mc}NkR~NcB$QA? z3sq4R5D)<=QUeJDLV$!82qlf)#Cn~XJL}H?z(+sb7``z2>@n}?0LZxE9_>Qd+Wn1nFM{YST@Ho z+0Fs8qmWk28RajQ7+U;73n!mGAF-ty3S+cmP>(1vs+uZ5J-0K<@$&X5bo)Ck9B zVTe6|H0XrF0w4!e80hT0*aeamvFwo8(6U=Yfbip}#i?zA1u8U5`5R661~Wu2+k!R` z_Q3$qP>%}q4s;2WGcaa6mejInWeesEq^oDG*z#L%SorIUAx#ic%h#HF2qCV=&M31k zEBtguFnY^E=@4`jERN9vi6tn0?oBhjHRKhEA$DHz;3srm8wg`53C)G>v>y*+Xz7f+ zqAHmYjvd~uhDGRtirYakU$Zug=7@*FB>+8sAt?Au9t?nvB#fa}o{>dF!X+wEFhs(4 zh=a&$kbor|k+8SgFle=b#WIVGFs)&M;5*{n%?pwMY#@$%Cl8VqggY7M-f%QJj5y1@ zxwEshAYhbeBa|2mnVy|0*qhkICbE*bSFde1|KYb!s-xA*%4Z6g)^@X$nxF;o*xW%B zRM$dTVfhGW)2NV$fTF+jXj*OcQ40bl zLztF=F@8ajW0py2N0?pv=ifesr!yx8dNr5Prb*$2#1VvdUF-57lMKxJhv zETxSBMuylrKoTfz=R8ix5iu95$H-BVQBgVsfwde9M`^k6cNvy8&b+#^Vn*Tyi&-Wt zFeZYn6=38$&R77#2%wEzEG{Ku7CB1Nq_l=Q7FsSutkSD3Nr_G0$1Ps_j!j?jrq|7S%NsWS``2vz;`f&~+Gz4>wvzd8`%hi0q$MiWLCJ8X_BSfvc3e6|7Io7ni=0G3B2gjfFZ?P~MWwP= zm8x1)a;k+@xT;QeHKuJ`x{Y1Nz9y`&ZJ$@o3zIvH(NVFgKz$k66A=YKxvE4p8U~eO zmacSVR748|R<+yKvlpaoBCCQ`vI<@p0%L1f=4iFj#)HI={7niEUCqlEn7hPumB|A2 zCjy0b8_gDAZa)brR&0(ukfj+qqt;T2sQ_u@YTGdo^`#r7T7sfD~0LWnSP6- z{ZpI(TsRJ2h1IB|0V=mC6V4awsd1-DaX;xE=#pn~3nCOZ0rwLM$L?Kq;k)gyb9A?X z$ye)^(p`m~mlgoBn_Gma+z0$6!GchH<5+9frY2kEbD58prTLnOInpa3*;$CC$Uc16 ztXj7A!Q0n8WKY=*$KUAgcw+6{&pvLEeG^;}Eu0&dlGEGo_dNgPUv`AMS4+9gGqa*H$l2 zTxJA=SREP(xOF>}WlQO9FK(d$q*_@A!sr1hi8wZiNKVzS+kdIV0K2jm#7 zoSOwIQryy$FfT6U>>yH*$Q%HYyIaaIaxY5m%_SJ!s6Z{dZuannZWN~>0%K)d>_ULC z445I7*3&43a2%3Fi&OquR8AXg6fi@SXaNugW;7jk7+@~!0EHfj!Sq<`QH_92-&(nl zk<3t#H*stLNDRr1G$Q~!Q2{U#E0)yNmN2lm6@vgc5%Q8ej4oKq*mO!sEJw!>rPgRh zc8*92$G(AdQFQ3JfHgIijAbZjmHCf@#^q7OoWb#JrS$*kwc3hR?)Tc1dw*AqTIzl~I_)sL+m-r6?1tCm(ic!!Y9EQgn7g zmsJsQa?i+-SJSjyr7g)u1v1nr*OW`FGY8mczI0%YFDtU*7!{tpw4!Lc*l}WrUz;Lf zD@-UXd=p+p*X^X`PU0L3HJwbxT5N_|M$st+GRiJ17R-h$vOoLiG8$^Z4YBC@7LQS| z)(y!x2P7jgIWwZwIw&T>i?J3a-@?H+1zi(~K^u8lway4Be2HX$`MNGV&3ZP|df-

1?`aj(ZL4{@^H4_*O!oxHr6;}Wf?8FudYP=B`K7yl6O7*5*H6s6 zt?do%Q7^Awb<2~hZ@0g^Zsj5`W54j&osT_m{ox1hq+Z_rOYhtLi~q3er{4LkFK%`8 zL7)5ise51c)9+vU(}T}hu-}ohzu>2{clg)MKe63rAK2=3?|Snq-m%#xTfFskZ+r7= zUh~TKm;Fq(uQ313^|C@$sjE_MoV`=6URJ;=MdK-!q7|N_t|5eUM&a7X7O`^J3(`hm6!m^UEGi-JmkF{_v1r{`}ahFFWJLtIu6_)1~(>U*vsk|G?;|ZWMjhu+Djx3F%Ks zE{*EJia~Z4LSB$vDFCh=Lkh|2VhW3Eu!@OGbrVN~sEA9f(b(v`y6JVhaL4G@p_>?W z|6%mJ>SmJehSQBDu+~k+Ej`KTZj(Twa(i+2z%~L=rg?xHlKCHhvtGUIx4D)tz0PaZ z@|3WNm$ueKDi}@88XB?)?9DO75D8hEyL$QImA5`}|I&(O-N)D5^_!s@Jn4`4S7c7RlubMLHZmBgF-3=B5iKXamQbQbJd=|%Jlw)3l zc~G$@hD_GHoB$?kMjcQ_hqCj5C4iQF;xdzrw29Cxtu`fHAo(ORAzVxrz5@qO2P5$e zy-$jf9(&Utu~-n|NearS+vf;fRz6Koi^!b>V?q<1te;U}tQnjRa6l*ruwp zx?;(s(G+RRr}Ub;QQbyW2hI&Vmcl{dZnT+d<1twgbz42)CeAX>L&z4#5GqLB;Z@fZ zgli%c?x?h+^yInt+RN^|Wudy5(LDzY4{}tPofrg|v890VRSUo4=b9}CNJmYZ#4mxQ zS<~4?H(wXElasG*c8Rx+Ez)Mu#l41I&Hd*i#rNl zj)kU>auBY0;lTi4#R!0d)hr-oGQ0iP#Cl1Xm)sH;((IQCDZBBD7u`B zjvU@8sv8osQHhDzzL=+6Pirb-^|GRv&Q3h{ws$t3?L7>uktYx8Gf(*J9_l>)VHnaw z;9uIAUy@fQ_siQ#W54~Sh#vdPEQ#7#)%=HN{O0-c2bW*-(28q)e|g=?8|*Kibi}tl z{h>|2`q{12%R7Di?Yn&9tq1P*p;Hdu!@t z|LGsz{JyQ;yzMrdZ}XnFzWEKW+2|FMe^ToI?|p2QtkSi5*$yvV&FibmRm5!{S4t~p zmCSaNRk*5P)ovC>UA1sO_NBP?N;WEXREE^xq%CZtN@Mjk3o0;R^|Dbq@R=tauUJ+o z_v>DM^^5%Kg7a)H8)95|RZZ%_nj4~7_HWGQY6-rh(_HX+=AT4eAsDO0N?@K4cs3nx;Wi5FaU?7xBY3rJ1cZ^yXYhohuUv-b~Bj3%UwRDVzcqIUYF}ooJ zI2S*c-SX;kbjvc-nxKkJcM@XeSwecv#VjbMz%F5#Yb^qSu?V_`nye)<$~aGyR}-T0OKjs=SxNQY9Y zDJ&%~vF`uPLP}hSE*|;#9BFFwkvV+`BriiPf|i~;W}ioO>0o(Lh#ai5Po*4v^o)nG zNJ`b36Z3*ctF_3wFi2W(Ixr*tLbDXT^dO}%on#+^EL`{wcU?wln>gw7lm%Y5S;QIJ zGy{8awc%yX1*R!$@s|t?`xwd*m_lH%johRcxgm;fxpCKxoRZGVQZP!MFj}QJ;f3Jt z+<omP7HRV@&`-giIzyLryX;Fu@t(EvH;Lw2%QB< z>yYoDWfW8kAA=SfmnLG=EO@bFE6K>ER4k(yTJv&lB{4KFqX`QDl6ST(p>l z6R6}Nn}{Ln=7!&mx)59lX7~g+^6aL7$B^TWmQg;QEm^>{beO=yw>)(Lrj2}gkuiOt z`I08mWC4oh22Qt)(GwJik>L_cs%MyUYom7#j2;++O z4GR}Ii%}ZSUAZkXaK=`No-Y?tPz*-tEb&|LZVS3xB$?zjT1Kf( z*HfrMjzBB~4kG%Zpz!qHBs2oHbg1n=dOS8j+ z90o0cX_RH;gJ0JnbA^(UDMM-Jg-uXKLdjPPKuXV$aPWa~Vjk{{EW?+bdr{0qP&x}2 zEo4IHNZ30B7#t}xr3fcFWTEls4i^d;(MvQ${A{-r8!dEjG-}P6CC{!b^Z9 zDrrDky{-rVQYKBR8POpHbwyyHk5nlpqCoB`-9x6w&QUMhYu5FwYqSTs9_suQwjm|6 zYPu&q52YS%jbfNQpuYGPWzYGl<0@s!Gc*6xl-71Lqmo&TTm@}3`Tf)WiLqsV?CYsV zmOixN>U(az^!6LhnKyfvkG}WSwwU+Ybz47?z28^2J>t9DFF0)HQ;*p7nD2k?#JRg1 zIeUjacHZ`rA9~}5wtn>ow|U)m+ibec7O#K*)^FYV-Ea04=9%v=PYUM0)Xfw1ul-Lg zTs^B?RWHiicR&YYE-eQyrnBAy-=-QX0aQ~QSr-IIZR=p z+r3r2Dsr#7^n4Yo@);Xi1uwgQUJAcLReh?wMGHd#Q@Yx-mV;5{svTkbypj~%kRSVk zF^UACJ!=u=TsQ!^$;T*ugj&q_g(h^gI90^gU2&1tFMZj7rc~XV6c_XI^IuA4L+AYP z1Y6AW=Inj&{$D-hps&wA@;g5{V~$_OzWDs(uDE_i!-E+r{l&W2` zwhmUc))IB+(FJJwW^L!%o8B#WKfW!BA6QdI>sl`NB6zLalnmW7&8P(#*~*_TXNy45 znuS@@&BDYLq~^;VhC32-q%_l9tOG^MNH7o>_u&>6t<@U>Y!)q%*6d_XoJB2> zVT!J$;|E5rZ3>XOraAfSn|Z7a>0`9d%Dl{I@RWvdePXtT4zbKzE?^SBwj81zc3y;0 z3&7q)B!+qVI%3xaKrRHZuEQ;J$d&>R*?_zR@JrYlYJ0MRY{km02^vih0*r}>SO|-Q zN?gn3zLDKgmJEib&rt}=f?s<54F`aW(UKbsMgBVT7>YO0Kjr6A|+qQQexrKD#4qtTcvsmbGf1N zkCM*$cN zk7Od0KLy5&T0Oelw55tc7pE>TSUNjL%tW4sz?ucSZi~s15uwm91U`F1CY8WgG&;^K zDU3LIVIYHwB`Uc^9$`x@!)9k*&PL}LQ3WfL?JrM%h1omUzP9{`f06vbyXgIEQ@+c5 z*PS;j9%bSX3xo$hTKV)|s5$BiXH-^3B`g^(T6oK4zS3|?m;yT(JO)O-CESc+;DUsm zBNy|EDn~9z98Cf#t{g}VhB)FcJq%U)Mh!(+s@d_Eyis=3l$a3omKvbsOG&pWqmmUP zJh1n%tu1%a-6BM9S&(x9fR9kC%Rb^-CHJvb0}KNh*%nAFUL}^|?!jnO4lRW_vT$rE z3_&(Qv83VkiImKYk|-oUz3ZGm;P7D0fH7S>!A=HF4br zO$tvl1*1nY+E#wBkymk17@siG)QrM1a`|3;bAox|K1?w^_jGTJv%E{JZBK z)IG&{!t9CN^IH#cqgdAUM8|Fen$c5e!k94jjN0?wc-o&l39s`px@7gtzZvakzI=b# zKiupeeO^EFdtWw`)ysCDhdlo9ZI9f$$d{M@`;Tj%U3=Rdi!Qw4{Nwig@;`s>Bb)jD z^1*w4eC|HmA9dhoPdWU{r_KBF{DVI~{|8@~cfe;4*!`cqt^Ji9w%+MeTYhZ2w`{%n zt2TSXEI*OG`CDG|x>x<>ee8`l-guKuHu-Dod)!IJxrDGe%)y2wN zWv|WU-n(Wus+8u)PL#dA(aZv`{%r@jinxD;IdHFM+n%mMw+Bst#Z@xfIQDNv*<$w6 zwSWEC_n#Hc#Qri_boG_DEm~;QH<#xh;b*t@s*-IhSzW9`RAwqIZ@B7`HhC+46_0+K z+INSQoa$Ks#c4Gxg*$NXW-DXWv#2U{=`1i@)UE1e-xscKWl`on}#=pDg*Kq>i zH>z;94J}VXIpf45PMv?~4-VMf_m}6+{)YFlk2-WW-(Noa%pY8F@yWN{bm4t>Ui0vM zi`C2C$G-25MXmw`-6-Fj@ah_Ms~S+frigR$tJ<_Mx~{BhA6<3s-M8PM+@lkh3@R5F zy$eubsH^JQNkHHS{6d$5Z^x z$O;f~O+PjYHjCra#}H0y)q)#pR3AgV?S!KfQDSaZo(tdgg@*e$+UYf~8U0OKKi<&1}g4&&30QcoJS{;}P17T%rH%7}veWd+89doHDzj&O3YGF=x%Yz5?L2uW+D zJ1o5188DO{rEJ~*G01_VYhSnYzz8pEXljl^tF1+DE*&FnR50*JuoYKJ4~bJZq_}i; z`CAa#mH>Mvq!+D;O=Lgtxs(R*Ul!F$MJ3V*IW+YqB=Y}XOT6w}i2Bs6731jnWnvNa!IN9aUlYFh9 zE(B3v%C;m+8-|@;K8Emc5y+BiBVm-1-ZzAjh(Q%r%^a8g&Ms*qSu)sJU zfMa8!NJ9>Tjgii!xVmByFezc6i|8aVN2d@5oY^A*%N@gTEN#g&>q0|PMAypDg@Bn6 zEPT_aW9CR8wYr!sRJxWUFTQliXpMIA7pqmGgLQ^CcNShm7bZgdMRaMDv#wEKwEk|X zZECz)UZ*;+1pzao*udy!(FMZ2a7GczlSI`a(4r-#EXlm2R4hq~)dcYr5tXyCaEv2! zu)=ALmZTQIvhX!8w5}A>VS&eQlwt~8T9&agz%6W4Xn7fJ(RArJITleD(nPfEtse4e zflP`^2OCI6Q-eoBjI!XjGM>tMwxoD$^SG-hrr1aJ>h-7&JmY!vRTmEnFGk{d&x4;w zRu&jt&$m78<5Vd>c+cVo?z&OctU6XN+g~2{c6{n*#)`xZa>@U80Q zr&r(VmF$(bU3}gdbH4Dgx7k5H@SFc~*nXe!``AA@{@Xt~_8Y#!eD+D-KK9@p4%>hG zIeUNdTf1z#>u0w4@wUHHFTeiPlU?P>``9z@WWQJk|F!>D&)P~> z1uKvhuxezbF5Op;6|i*0tTNV6FIOv;`)8C{Y%43G+n&Z?2iXQOrSchbwJ^5c?#5Gb zjTsL+r37>-lh?nGKbH7qY_A*JU-mCSDVFUx`^Tj0DKEY8`Wt?Ix!=%MEUTK2IC!?& zQ>m+5Rdx1NU*)9I(nf8Csv=i4s;c!P(#lC?rbFQzTy!hIkziAn`sQ*! zon39~ooDYkSKFespQ|)hqDq6PSk<{m*o$k2nj<#FFPK*73gimsis-6czM?k^p&8Y@ zW(O&AghuK3wO}Helz)6#S7kk2DOWFRL6ppM5Bl~2d+l`OA$y!~^xppQ<+D!v{)Oip zb>$@sZoKB~o31b&WEoBF+^t`7_b_l@&n>I;D^c zq>F`;UJ>nLagD^*l?YF|tT+PD0Wz|PHC-OMoZW(az`193i*S>qYrE&CbPr^A)%T}G zcM|v5ZeJ8II-$7X5DI%YvF=pe?deA8K4pAzb*C!ycROlXP{j$BV0I6buz8vL)?wWemF z+=@H0Wrb6=bjL+RtxOs+1xPwY6tnIau~2*(avXwZ8#>Cm_EPa}c$u{3$ z*mx!Ml98V1x{$nbgmFgs1RTla8$cVl&oAV`-i3&+b`Ix)s566Ys73#wMQ(7M1FD1?GU2$*u<0@<-(H%hnK&as{YTX{oHIaBB{ zl93m15zVWqF?PEXhu|DVz)kiG*`OQdV4ER@$e-;3!*j7$m%~mAr@+p>Rf9B3;T$bpT$XH^dQ@ zOw8Idtu?1h7%V#^VbPkt91IQerl1P|pXd@pQE?1rlwB*RDbQQnjvx$v6YT5B+J5rO z3)sr%wwqPW?JW1Iw%4=$+fqW~u?Q9f6`hz)%hDmvVwBM#>r|nevIl#kRzpevO~D+? zN*BLnfsr9cgl5E-GA&#Zj;Od81yh-dXs8@wW>0{foOBpo__c%ul2Y9PhR~>d3OaZU zVYZmVGEL41AE3q8ik`jIz(8pfc+9uSvH<4=DWA@wjFM#0 zwuS(B43XhJ2$28)8PicnmhhJg|AN>=9S%VO6p0QO&ccXEsl6J;Qs-^|;&1 z*o;IMH~_~U`%(4Iqdny=lz@x2}I^8R<1_rLhfVh4T>Is%BqaUiZL~)%PrX zcJ1xn$9~|BD<5CA@X8D3f9c;gw}b5Wv5(mQb7!Bh2RY-Yuby-Ap68yr*McK@wWdF^-_odoZ8Y^{` zw7yVm*VrzwN|q>Z6})Zxs-xQxW>FE_N>&tCL_?}}?HUvN%cu~7;ZbhGplxSa2t~1o z+AA;nB@!-tDTHxStuHvyd)Ky-mC1@_FJ=>8Th>)0-?H#po6i1?X20}h>zPHRJc?!Y zrZQ7q>F2B~S9KMYTJ@%iQjH0$-c<0aJ#8oxfNE`TSu30sutrt51lX3Y`kIaejF*~M zrOXSPh!we9II38csPJuV>#Eb~B38`8AXGtXi6L9ShNP!DR%H`}S{SjgQIx|*(N>20 zYk+)E*<8lYy?WUM6Kq7D~q?Q2cCY?0YCfE+{=G?(!#5L z&#dCP*gq2AG(@O;pWoN8dbY7622}USB*l+ zHR5kdS|uSnFiv=)>$(HWQ4OW*vLXRu<4D{m-3odH9UlR%YobeB@>*SYU`W@#M`B(s ze~&9}5pF&nqQ-run+&@f1+Q*CBMJt3w4O$M(7*s=gXC)+%%%KgE>)8yQtcMOsB4Mb zb|(4H{MoHKu{qL_XiXNTt#)f`)Do>-<$c}TAHL6>#J0FwnY*!%Jg=+SxAMsC)6g4YORzS|BEpv+Hl6?4n$LEvW38mja?vZ(bJKP(7CWsnsv0M;K11ub%>4ntQRpJUAmSC zjZ-Yrj4;1c0&B-mQ7kw|_K_g~orPUWT3z9YSVlQAj>V_wTD?TJ%_85B1Kc6jmNR>a zMZ#ECx_Vyh=f3fXLsBq=Zo2pF88u! z!9zDMLTOUkLIjIbIElgQax|2?Rv_-|R5m!X2B_e9>)C6u(MIuK1*|PeaS~eFezzes3oW{ z7?OjQ&dAsL@RbiEOtYqpWHwM7VrLXD*MilH1SbUpiNU0ZC}Vk>i-?f$hA=nxjtHH7 zI6|V*#Sjmr+2tth{(jZTFg*p1(`n0UFgrAmx(S$ta`$kO6sUD$mz4=BlNHOPP2|3_ ztX{UEJf!b68=8EndFH-nj!uB4(8@R8G7*|01S6P;Fs&rrD3XjN8CYvI{H5F`UY$L6pS6CoSqiCmuj1p_C?<_^ZOOz0V7h8aV2A`SdKoKTEH zww_HBASJ?<4u-Ks)9Q|?1;)&hF>n}cr36;=EOY?!Qpf3Jj9Q0MVvnRKR)aLVQUxQy ziat^*oY9Uo1pr%w%~4xEjOG|yE%%Jhe771`B|A*lohUK^c-iTIahD+&-O<;T#>y3w zpD?QI^|bG~)I+KFBt5hfPj^Ff4Ym91d5SaUK%g;S}m+vpDm)GCFc;&Lo{Y3V2YnNR9^J91Z3#DK z*x~1=?t9M3d-?LRe^2VX)4p^1(O*7!-Y!QR_}OoK=^u95{yks%%vRff^j+_I+eTZz z>vivb#~U_%y%(`(ZM@N}S8g(EFC!Bc z?_e7OR!6gI{mj=Dzq$wsFGaMjdRgTQA5X7yE0#q85QP_)g4(U77)5Ky``k)qLQyFr z(khL;AMMw@uDkqF#j@>YKlTM+CtAs=qU?QUm8Qy3rR+<`S_Nsfr`lHK3#R6U~!N~VHd87JBHX0`n|6O#~w0! zpRav)&o9lNx9^z?zUN<(I`1ckUvj~G_44gYF1mZ!uOGN;q4%-ZKe$}IT&-HM<8mN$ z7moAawNnkK3I?ktGUSR->ltNqy>PKT>Z0o!(HiBhJ_3M8Sn0&hqHTzsp3n_V3agoK zHucoxM(T?0{_M`+V%KH#ETQdF$<4tdU-v5yYyck8Si)zojfWRsw;InLgc2LPZg(Mf z5_c^(7GQ~)i$4nxcY|i+hTu_T_FRb0>^0yBn=JHOjgDj}qL``qvX=lOcWm5;fEmpZA$#B!pD+H~Z@x~E z!gnNQ(b8K4ZTZKBA$PO{r*2WQG!dCdk63>e7_=o&4m}FSscop~07TM)L>n7g#^RRe zKpGlBff2SK*hr3Ls~vMGwDh=`wIP+SXHMkAQLbL*4%0ed!`ySJ=#nw&a1iUs3uhT3 zlQEfGoP~j$O&K7m>ozytMRaMa)<-wJ=Fk*1;paxWN9ZKw6yU==w4pBor8N zjdB#7E)!jICzR5bj|pN621yyPz|$?TSdP96jUHZ5{NfT8%m~dZP9G3nnAi5cvHZc{ z(Q-s(k?D+i(Q}jr3n?FjB&1dvlFvmuyfBkd_^=yA-hmLY)fTj4ja<$#7Z(!VC~#%6 zcd*AMvg&xenBD8yEX2O^LE(uUB*R|j^zzS9@&H!e zy0z5uOu;~ml0lw@ZzP6VI$eT1enuExm?>b5(hV^hlAC$OZ$^T~X-E{^#1Le0n*tbx zBO`DMa3mq4ZD`s8&)pm`u=5)EG$gmKDXAhH#~rv#v}ntX5v-{-@tBQv$N~!iHd3Xq zKwA8AGdc}m1R$83Y`$nm<|V^2*f4M`rO5G*pas$$0$&u$SST)}xil|S1g1>zm|B^* zW}VVj48Uj!l-S6mQ$kQKgfDSG>4J_N0ESM{Eu|J=OsDiZsY(x6L~M|qS7R=eLCX=I zg%n6jZ+6r3r8AD@0!vCs@&dV7NQ5J<9Ll7)7<73}i=kF#ml77x9Q|6o$oDaSoQ^EFE(>gG6MssPZgxfZG?(wuWDDus*V+;ic}+k8?ByMk(P`I3tmB%R=!o zwts@zzbAFjKD(TF^!`6S{Re*U%gfmQ@#SkSKmFE4=P$qY@|DZ4TXX+yk3O_w-Kxpg zW+zqZi$$Ch-j$-XamgqET_BzLt^#$C)&-?av8f6Grdo83@bZ?jKP$OR*c-ynu4<;Z zps=wEr^`{SE;L3ui|0Uh(Vks>?zpSCiMctrOSstGU3;=+cVBYJYdz4q^8ovmOWO@@ z2)T4`IJPddJ$RtVaG^00_lubpHncD^*^WF~Ty}>L%eKF?4nz88AUf_&Dh) z>}Y*JQ;OT_p_Q9titad?S}qQO<5>A17CTHs7%0fcM08PU4YkO?EW%mdvMMogotCC) z6ga8}?po@o(IFEs2_s>YUJFh~E-`^x3PXg8E@P5cYaW1$Zuw{fXl25;?(i*tZNAzp zU|qJ@nmCI|VJNZTBHIKf1sH%(bQ#B)-()S3{28r(%hCzl$slO=lXyfNcvN!GvRJOJ zcWKaB8bwIV!u2J(b-p)C`w+z4e%kYiqBMgW2$Az-2uP8w>=>=;rC z0oDmgkr<AYC5)< z6{JR`#|y@Tq9Odc*i2byN^vs+$PF4}^GJZrg?qFe z{YkhdSNAV0R~q+&*nj%{(``B{qKS>>6fF``wwNQ28Mz#WlxnkU%DjxSG)-*`9W0~G z2*#wWAqI}q7f2QiToPW*D3U?WaT+!fuqNz;A^~=eLQq8Sq!Oh$N{=xx7~P*lw7@Xz z0C)&RmyyU{Rx!|7;1if3SpYD{K<5SEs5Oe#CF%IK2u5K5*o#Ov3mAzBKV=J#a0P%FT1ZZB6T#O3cj8Z!11HPqAqN*<~)e7Im^=OQ9ww7R()Dz^YI+}+`wB!e+tsoHnzQ<*AkK zL6@*I_I%qztYX=NF^fmL@i4E;Xb9fpu1fitNBy?{G8@h79=y%|a&<8yvC-T&nn%^_ zW$Y(c-~ROK+kX4_U5`I_lb`%jFQ0MTKA->STb0cI@nv6O_T^<;%-+X7{m8GJd+MGi zAHL&phwL!#pwI60^$+^VukAnf_HEv|@w?tOYwImuv&B1J`^MM#S?rmg{PHgL-@Ls% zOChY7RVFKJRj(>mrK>_%MXVN98rQY0tcX_adbwJ)3$N?tZTrY=Piw1}Rl!x$>T61z zhA7_F#-BTjQI)m&nl2(LUG=g(Wi_&wvAu>(v5)M_%eIo+biVtxTfV>F-oCj!Xa9Zu zGMK_}h(fjcRL!e&_1?1j(F@PIOBP<~o5*Td)iFGaQPr_lnMwShGX1P`BqQce^v{ zquFRT8A^9T55|5Aw3699p}V3M0NxEMT9$@*VIXcAq#G$P7yt&h8qp;$Ny{Q_>6LJ| z5lh5qh+V7O%Cvx$#si?1v`QJrB3lPlmC){fZes4e^3)YwXvvh7REex~t%JK?w?Qu5 zOIh4_0W5;aS}24jg-vUY`D%5$dGf{C>LDRURe6f#QTl}(qX2{#5+RCi3jHymv&d3V z?73fZ!H+Jx=qI->y26>@W@~ZGn$kozpc2U3Wp37{X8K^0#h>Xmh2cmsBU$mHv_!mE ze4b8c2$0CuqiJUaJTDe56h;_&QM5$Y?AXDyVc1KXlF=xEwA%8gQ-JB#(BfNOR4j7S z(j{O1^dyo9^A%-;U?~l04}N+lv-}&4WV($(2sv0rL0O94(zU$Yb=fJ&ASpetRxJ0i zyh78>)M&BFM3<3>+i3IEO@8uQXPGxD4P8d7J=TP~pt_7sT>#7Cg0lFL z>&9saCn|Pe7fYoIfU}N05^U2)s+5eHECyZK%0ZWcu!!s|EeJh4rEG<3n((9tGovl6 zArTo%xLH@@8|tBY92b}bO#T4Pi-N()@AUIL2@e3Nl_!P0tRU%-f=XBpc=$>ZtXbq6 zp0O3?$PPdu?1W<7P~k-KzyAD!sxWsOxW2^<-#TnS0>ed6_Gzltst_Br0==w=D*-%& zyKWY3Mu7l|5`Wp^A2W5S>YDO6C@^Jc0h=QRIv855HDotIMm%)nByefaAvtQTvr((z z9(sHDfK-1UqGAS5H)3Jup3M3r12*dNWO2kZ!QjB8Y zq6<^Fz%1-WfyYqMS>%>Z(Jg3GK-+a8XUB!n6{IcP$Ok8{GNg~7XcKsV>{&`OuYr$& zvC-@n$_hA_7=<1|hi|8uQ7d-O?Kj=F_!|EPmcR4M8AfOfk_U^2&O!_c0YfMfW5J`_ z>d_g8g|X;bx@4?arz`i7d{kmJ>)`_5`U@>LhgeEQERB#N@&(Yg<{g)g6q3^5-a(Nb z3!z{Jqa$g`<-p>=;b#{MlARJKJDpI{2Mm%}5ccxdmIHjtNWl+NP%Uuzm|&c`6qrJD zq;mv@X(9klHuI9Xp%E@-Z7zmLAz)0zHRvXw1;9Yq1q!2uZx-bV#u!`qh&a}*d|G^Y z0!UJnCQH#}bTe^W5Jt|DczE+{=CRF_DqYpArH_YLI-|`pFb{hkag)GR*}#T8755}O z{e1iTXPwFJcuUpGeQjCyi8cOtbWeKzVla76o7i!#aDMc`Ti36;#mm^wt-HfNzWl^P zH$T7bmP>wi0|HeSYf`ez2o@`Ro(ErDRqwpLE!l=Fk27F$aI)z&$^@ z$CtO;;gegucZ*H7c>62V%kO^Y>qfmiQ#1dyKl3$ks#I@Ht9FS>R(0HVu!>lh7*)(3 zJ#VgmKuWP3o}JR(w6d4*(ki+Yzg&n~8-7&E_yM#EYQ}13_zrQ)=5TSaV3mr?E6YtQZQ3ZaZe$Wg1d<<-76UsbS{7iPZzX2|BU-DHKf z!d5*FUrUh^j8QeXl35@+7rsce0s-VA96Y;vS>_7uUL#i{$E<3G!~jE)LtE7dG0#6@ z_9@5BJ?`)Wjy`m+GftXw)@gJ6GWMkx9)H8tXDwa)^E+?7_|97{yL;JH>g6X^-}BVk z$@>+~c14Xn-)ci$XFlC;I4kk!T9<=sK_TofQ2^Dyh7?DHrHhV^ig2fQ1!=hxFjt?; zt-n4YESA2VY2zUnSP=x#gi0G>cRFuOUZmlt1q8B2Ew_k=J$@v-6T znW4M4n+y^y=|KX3Vw5|fRNyy-X4I@DOw1a@vYZ&K#P*f+b)lvs+E8bCNX zl&$>7yheudA7a#<6PQJ+rs)RgW=q@*xtBb}5R6&?jIfX^a5wYrXVvt~~pA2A5&Fi3!njt?N~){@4bFG~yv^O6sMQ5j-jj{z*C zu4QZ`jSf3S=g|D z2u6A&BpAZZRYTDZFxsfXIFT@j$i3;n56%edvO8}(Kg$hX*ZB_s2{63~h6?f0Huq)` zIUPBw@+?q8O~epmjxo2e;x_T-mAg!MffvBhu+E4 z$~5b;jSy0}+*tt1vy^mwCE2fd*;ZB&+dft=+h|rSk6*^lKI-M+E_5eD8A{s_!j`l^ zjDpICP;|LC&s`<}4Z*V$7I|h5!>d2nlz$`uEtI%~(Mp;=J6#T_Mh^5~T!h0zF$yMs zF3KqN1tSah0D-|^vIs#+60#`+;2ffhP|F>XZ(hKxD?}RQ zED8){3^~&4rWlP1l8m5?qQKxsDD?Ch3u%-^AW17IYShO%?d%TQo#ITVy$I9;pSWTVnb10eE}kzNAXD0e!cz%csjWq&)O zH}n^hnGLL-z^ChFPpuy3Jj(V&XAhdi1F+VEt`^w#hbKC)o@OC?3^{`tle5@!vlDmM!1;%J**h z`YpG3!<*hP`IWE97W4nt{&Fv4t6tTx_L|#ARxGz2Z1dOq(u!a9`Ntfga6akyqm{6Ww7%HVYwn}4P zSoUu&`yFh%%zi7|d)L0Xto1(jvc)&_?=pKs+dtOq&1BW3B3C_Y<5eZAE@n}RdVyI* zY}ZvG*=x`KNhykgT6wG9q$pRFv5HOm(duHArm7iFUj1$u5{0u-Er9A(tBe&wJ9M>b z8@}GnR`V)s#g&X$)xwaN1y#}_5tM=ffKb|K2CK?8BLHHr8NhU~f%yxh3EC@1D|wZ* zS*V>Qz*kmyz&HXN{{6jAJnCTe@-c_)eaigVe(&qd1qb=}mzB&nUh`x1vhOe7d&e~o ztXS+HlUi}>bxv}pw|8jO!uCL`nw2(;ELxX3#GAyPt^ey=yCALCo4*%z{v)+Py-z{LP5P zg0T3D0T&Y-r8IXlWw8|fNr7%1+$sevrEZ2E(e07-G+wrn0U)|Q98HaduI1Z})FMz! zA(&C?gJdiqFH)?Q&XDd{&*370BzJ@Y5qNeBrPUSD=ZVNSrf#JF`NyXi z{cF_zP|%8L*@Y9dJY(*DZeA@o;}{J&5^GJgrDql`Lg~?E$QO^L5NJ#dptQNf-^49Q z&oI3BHpEqRLZ&Z4H!itVu#n&5sn0X;*cR92mUf5o-^hOs*nV5x7ZOn2e6y4T@JAQ&;EMQ$0 zqhJy?H7>0idtmr5%@Hzi33Fl3D657Fr%k2-Febvzh?!7yhY;%B!gG3^hwe7gWtXzJJgUeVT6L^pw-hx2b_S0c3dJol7tv$Jt3zbX^&C6j%__12k)1G9eUT z3;??Lo7x!SRoVa&2*%=Z9W6&l7&^%pr=W5!N+CKfv2hp3&BPO(=;hp^nLdmm?aPWB zh@e`Io#gc9D?Q8{1#Q7)1xzT6V38Zug>POhI}A?gVUWMs70W1v02?wFJUH3m*##v= zqczl0lnlZomw2?(;GyFmUhsC7Z87_LvM(|#igk&?**_-Lza&NPee6nRJkls1RI!<| zbv9J!F=a+mW)z4}bWNO?V?~*XWVc++0;hvT96s+AKx&0(S%d~F1V;kt>fNy z?4(K6X7Tcml35PAWlP~Tc?5la-Q>Y+X49v~au2aKjy<6E$Vyi)k3zr`EHT=nzNcA* z10-KmczUXbw{PMflk%I`YGf~FE0cXyxvwqvL)qTV?)7Y4Z)@9z_T^=-Wc&A*|MP{1 zY%cqK?6vn^?f0=i@XlFZ`OKE;<$wLhSM9y)`+R@-;$u%cIdaEi=X~1!@=@R0 zVefBj=O?nivC9X)^qFn8-r`m7-{y^5zvqpcz44WsY~m-sHrn`=8~KUsv2pxYD&DEK z?c0}*6|LUER@kbS+Y9c;xLB0Q{dBfg2@DCKE>;37qSe|;T{SY{%NPSi-TkYJFJMivjovWIzx8e)%%c0IJRG`SXSY-+pBc7FRXesL{~)Xs_T`*ENWW{ zq+(oM%+8|bg;#VdhZVxQ*;Vn%cEz%~+YmtrCKi}tnHM&I3U;(d&iU?wW9ItRuek^9 zdFIK7`U>-TKRN1GzgTeX6=&VL=t7&zcPzQ$uBF#La__SBt5&SO?{;@E7o$h+o*^l& zP#2o63(y7S*5vl+N@Vo1u2FvuRhF{2@QreDfp<&q3PtFW?5YGHJ|F!B)k9sCV3%rF zy?YV7mM^<|qpm=ZeX4@V1e;MpLD(J4M~}a*_roSyPZ9q1e*eA8?zrv7yO%GaduH&b z`1Na6dZ;i;FI*&eN|JHhZKP5DW<;J^`-qo4pM7-gbC0bf6kSF_fBy0Hzj@+OcE8e{ zaq^R->sJv$A6~U$)yg}_gZJMl0nv#x5loqQ-f`^59lJpSxck3Ri`hrQL0t$TR=ng`ddUiI7OpJF60 zJohBblaH-=e7&EnSt}V?^{SyXtcIm9HTfhg31ni9EkbLk#b7#MEz$Hk;ItGR4W)zi zEQ+6(I2nY19AxPrv;ifB4M<_pOi>dw?)S zaTExCMx!tUxTBMcQQf>K0-5-lhwmp8-9`~M3rGju=<0`7I^0He8MEs${^7UJwp@aa z=;8)zzL2e0iw0SSU={}n26#CDWC>%4g?r4?oE->uQ;UHG0H@>~5j;jEi~$D8;3<0R zW3pU+|H3oyW@^Y0XL%{OlnjYdK755HB9eyCHia^j0Dyeh2}PF?13V0pn0Xm(1+X<) zip7D1frOt<0Ac56)r$8qN@wLi0jv`j*=HmX6sdT~_da`u+1xbzOp1basw}g6gH2GlBtdClnYK zZL2|tu^>i~6e|on45L}N`m85dQM5&j3O>=rV1m+4muIOK#gxmxxbi8q=;NS_=td=A z$i$82k}f4(NNg=KFcG@kq(@+6(yB@WzO+k{g<@WvDtLfPDH5TZbx{EDn}X{RCk4Zu z%a+`A=Tfw~@U2)=5WOUgj%ng&yz9aH?{?<2IE<)<@FiSc zR=B*ZcXQ69UVh^vncgf=G?^&_zkq7VVj5Xxs{PQ1Qz-*PQ zkF*&r3yb+8*VV?)(kP1%!a3W=oz^H}H%dBimRAUDk%$tBVSZ-DnBNLxsrFoy4E_82StAf3T?PYBH$Q1I+bANiH-xT}#Id+*B zU47+vH`^975=kcVX)fwz1?=J*u0d#LTqf6Ebs3|HNJ9c@b4GZM{WU;W0!nX$NNkt; z69KS41t8JN9GK1vO!vC0FE=lf;)_HeYtkmQ!d3z6b!eh=RRXJR6}fcV%IyNP=rS55 zw#aQ6tEPL8TIFk~vbs?l(+X|0019D96fs2`kh!n_}z zeB}4`-R-1fW*>d%cg{ZZu#3(+o?Lh3k8fLiv3l7*zP$GSJD*y!lBg3^g{t5x8dVRS zMZL`8GO7Gg?9eCmQkcpsa{qARWz}<15|)ZgOnbl$u{32#;;Wr zS0gKvlPuNIDrF_}sH0WQqN}jfU#c)w=R*%HS2eGG=x(8hRIiP;+)RN*MFwUfNH8Q3 z8UkQ39hlsCnWOrS(o)op1)->U3C|px) ziNkGStzJjUvGdmqe@p2vFdZ_h-G`K0cb^D84*H7GoKxk>fY{&Rszx% zTB_P1(jLMHW;#u|b)cIOjLgT<1w)mQ3osal@#lrS*ryh7Qq&OX^~vQkNajt4UDt=# zn5HSP5FcSap87Q8=;Kn`I%v6!HHiVzhuVMv;|vyldZ9$$Ks}2-=s)dYm#M*!}Hmx+qhqx}iiPHt@fLaqmcZiL80@QLOCfIQ|Dx%gX5<!VDR?A*Xjsu9>owIfb#&95EXr z!v%(L^cd_}-4NY^>%#Pq-$WvBLFh#>_grL4qO)_%%P88hT+?>3 zjGC0=4r7R8z6eQRJfyh|Mee#)FmxiEm$F%+6FC#|F^Un!b#6kwAu(+iYr{0 z;>J(6bnXWJO5hevC!O}qBKKBaJC4X$0Ty;HkT|&q8>_@lGV&sZh|toji=?9vNte=C z97uL99W^byE`W$gC;$;QFUGu_V2+5T3)Zs0j5T2&E=JkwQsgX3p)+;}=#XVT5^F|= zV6@x;j0T_!WL`$|Whr6ZU@zP98X2Oc!*gr}xafvyzU)Z}6hctxky2vJ3!$SNVQC{Y z+KLq}TD}M|cbIfT@=dxG;KxHs%;IyAUKXP`;gML1I1Pbm*|i>h6Ay1asCz8;-0KnD z&UKc_WAMzgx3+C%Rr3%V$|`83aqpU{mwUTXSIMkWeqzn?-oB=&j{C{4Uck23Y)HM_ z*0a54_3{I^E&cN|>mRx6=0{ds_sCsWty*@`8OMI}Q`>Di``aHrWUr5XW&3yR@Xvq$ zwNGz(*mpjC&hh&%Jpa_oess)`?q@A zn_m0M>g7#ly=vo0-Tb%xr?yoLst{FtZSpE7Z5b;q6_tcSj4CG;j7H(BH`^)3V0%~L zOd*C;lhm1KFg|ojQ z9CPHMcAQn&Z3HWXRkdnc?_FyZylPv!$u@rtseDQM&T3~v>Rg-1N@?GEZkw1FDzAbo zz#$2jHjW{6mD{RvHFB|hHJh>Mq9DPb=ss(~F+Vx=IQ8-Y-}&nCM;>tGA$$Dz^tl(F zbKIpDp1kPVb5<<9;-1^Dy=U3=4=i6|i+RmT{~Frlt!6cj;<0*1SEa+Q$ObJ{o$290ek!$5?LK?hIqJa(EQWlxk$#%1Ho~@{?;Ge*BTY)U#ET zin~#!$%IHldZkpk2U)_yURi3iI!@P+mT>gaotaWpe=wAWM$3|Lk74k`(7CHUxl4w? z;E7x@uKK1IJpkP{E7;({uL}myI|knVb<`Zt&LxMYlSkXBVm%rC#nMf?`}X)a@F__= zlG%k(F7?r1l(?mrt#9Y`H-X|Z%IakWKvrf!T5v7BH8d*u>4_Mv3&XLA)Bg`=_u=kY zRo{F3%nUGe0cU6e(u~(A8e@rilOUK_z^Fk*rAY6+qoSh8wVPm2gVKwDbOxrt%+QM{ z_7+Y1SMKYaH>_vpoWp%C%=3NL^IhNd`Ifcz-oL#+v-a$$LvfeJ#z z7lDiTXtH(osMg&%D(5Dye$kXiCloOmE2%iuP+I|`U@1CqmLk%}Qnk%aJ{F}q2Xa1_bCVtS-( zly8Sp*AgYw&k`O*TDSThk(~n~cQ_y`pqgcABHHSNS1mmN@Z2RZd)2Ts1zx%^qR~}C zD7uUxb>XXomt#$5Bsmt}tXZURxV6J&7_;XHVB*DxWUL-`LZ84;@=|nFI94*G zAQe-!zy)MC5nW!+F=?zGEQT%a49=*n95*m z;wBrWGb;DdR-0#R1&C%k&Qd}##6=wL@TNvl30Mool3i=4&qMgCQbr#Ox{RW=?7VcP zla$vOt)%LaMKnUm7c8CeKmPC=A9QuI4_{!yDDwY9EYzZ+Dk8E`ag=HuTu1bxiCnJo zRzDq{7o`Pk;~TOi0s}BLnBvnRJ0gax45XH&PdTdx)5nE4BA2U}eV{d27LG;WPK;{B zXEZ8K#TP-By?IG0QOM>f5e0_Ohh$Ma3m1h=Kh0bMGFGSQNF+#wm6}U6i_@xV4YlB6 z>K38A)u6Rv@x+-LB=J1cad!GwV`kV8P zf7gc&*wYsCTlU@j-EZ0D_(R@t%0~`3=KXI+mXA2-tp~khp99{w>)T$p;x)T%vHj*V zd+)O4zI*KO@~xla%gFLe-}&a(yov2svj5>T7k~bu^NAg2+}W3xKYi|5 zgdJ=41!qP>QV61K>5$@L%8mjf&gxVTfZ~-GCfZAfkyN!eUU#MKYwmd&;)`o@S5oX- z1i9Fo9lJ$ik;|*vXP$OqU)DxuaaGh6SH)-T@M6LEGWv`Z!>0Kt?V4Y!q2$6cAq~{;JebAAA3& z&pYmdGY|jTKc4w7-?;F5mw)!w8@{pT?yDZW??(H}tM9n+fqQP*c>nFVCqZ^x6wfix zIl8KAXV6R3NRZ2ij`+BOAa%Le|3#}^L8uVA#0W`TZjntEbeLr9G7O`QCYOEM#WDkpks6u+O;GKGa6uyf6dZ2 z3a{|m>?!cg1&qZ`t5Fl77lb_#fdq@v#{THQVfc!sjQDbi0CJ3j*TPpY%;1dld3E+Z zBP-U?agta!AGWQe)waT8kzw4b78Q47cUT|nKHf?9bTG5A^*H>@VxV-Gu{I7RDL2rnR`brXDuZph-#YlU?~tU^ffTV64Fc_9!}cHjK3 zfBzkrWrvZdl2}y2QrUDOAOPPg(N?WCd&|}8!M@Dj#{J92bjtz@3a;9 zfi?G{10LP^s*tZZVpeKJR+X6SLUoJ`u@lDdBIh-_QnxaSA!eb=m}9=BXdR@e{*hNG zUd^}jBrk3CSRw__TZc+23*Tw*bY7zI(v>Ttu23x_K$Q%97GNttPe!%ou1J&1{y&6V^j3YkXB0zG4q)Wu; z!tZ0MEJla4uxfDC6pBt!hBmy6qP6B)Mi}qCkS<1}gk2?u7`3JVtQZ$g(G&xaBSjY= zkxN$)USp9dFG^YmEiZA3-x{hdW2G8ZAq#s$ycmHklr3fjvp1=b>QuSo#jD6#y2)0& zE_d^7{dIE}me56TM63oyaE_wM+fbtbQj4QYL?%VhRuT(9OPW%^CJ6IFB9|fH4vI=j zXUd%_04Cc~s7fL_;R2wWkuEP?NhNJTDkFDbV$`BhT8#nY3s&)3_T~sFxHyZ7XzDLW zF)KhcvgoYY;eiu6OS!aM@?|G1h3(MP4WL~Ze+)GWuR`Xxs;C{Ov_TD!if5>0I@9lg4_3L-~vwgST zdS+(3keI@&)@nr-PD~vDO^Tio)Ko%Smy~Sjsz;t{Qg+w;7 z-S99Jo2n=zB1yL)i#$@|urX8k_LPN%#7QM(B)nim7DVvb_o(My!?t&fAYXd%1-`1> zRKc^l#yS*JZ4)N+eNW)of15AyUAj?}&A z%oDtl?I*wNFZ=$oKPhE%`SNdGgfd@q#TW0o<=YRex?#ipw`{oYHeX);kuSr}kzD)~ z&B1G8i@GiiGfa}fRA=j^jU#sg&|7CK$>KC{G&^u^!QHIg!p04&8;!e|p>7%7=GvnV6?6|axqQ!EFuUO{}r!g*VVQ}bTHk9S`$NwZpdMoDjd6Smekq` zLOiE&l#b;;|M*)^Q@pftnVNvpEObVqctg`s7A}HI6hNyhY+mg1M&U`>v2}agM0R0| zASOk3IuUr5(iO9?^a66p9gGY-BfvC%7A;tH<^tmwIb?R;xmY(36|xjfBJ0zNh38A? zhO*R3J%nZTsylsy5{A(!!Qyahb(zJ6%ed&8 zi{lxo6ipDUbzp41T0`ToDh#G4tB^+3UphKrCtL_{MsavqKtl@FvKwVDg~+Bb5;2Zq zOraDoX5^q~o4&lo2PV43$-5AI+T#5V+W{)N_Cj}FF5$ZmckL~1O`^5VgM@G5HCsLMQ#dU9nI{O zU6Gyq++kRR;sVC5LZbMp6;=>Mf|V3cXOX3v*_%jfLSHB_i%Jm+9)M0pVT`H*W0YN6 zSKAPzQcd3w3uIt+pAsyF#vLSg#d8l&frQC8){W6pn0pIlvO+nriP=d355u06SF~g- zjw~ew2szTR3ngrrMO#Zfq+^tef<@zN%{wQvGnxV;Mdeyyt5(5;MOTr0xv*4*wi!_b z%m^@VedHBZD~Je$QZXVgl<9hc5!06_Us>h}JBy^kR%+FXLz?3N7-|Y)8g*#6S3^gk z;N{ZJbB6(7EOJ*zn92|zK%0_L3*Wj?^3rky9{bB4;d+W=@kL9N*&|#}aa!HyM(2L` zt6%hrFWW@Mn!S&WEcZ*c6kE@P((hx(i|P0>`b?t9Hkt{htgFSEf8i^K58nIKhE zfkkFsgEB0{Gf8vWz3!4mPSqoZBx3h$;K#vh8Nnn6#}d zpdq`-Fh;ou=#M_r{qZbA{-U&K=bmyjIs4=z4?E;N`0}NnIuT#~*4HoadtWzP^(Eh5 zw!i$)njhlJ53jk?Pku!kBd|zKT|ChRf|F9Bw~<*e?6M&{&Wv=HXf;}d$!cTAeC-8C zmlz8r8l`B`u>&ORx)F0jvIAFxu4^_H2BT|87A|o*EjokOAW(zp;8xKqj485dh*x1n zP71}Iv;rm-kM@=|f@~kzZgLySx{NF~nknAIMxt@&-#q&a`Ycxrl!A`oV!e{ButDwu zD42k05i~8kE{>*K9F{~k!btIUv@aCr)t9wdz*x}0Dake8yrkxsi}>soq(rq|^CsM< zq70)P;fMRYI<1}+^I1B69YVL&t8G17c3^jkZa_Z3$+%&;(Z?CPBly5~ckro>#m8+; z!Ag2#d@SjdFSkmnHEPP!MN_$M6EQ?ghX`m9{2Ch+mDj5Z^n@)FRdWpJOcUz|uy08F6(^?9T*1orB`&G+Qg_NmY zB4POk5UPx=+lXI^+!e3#7L!E~Eda1Mq>|W`(F1nnaq*0b#T$||#})y=qebghE_S|P z?1k0p=E#?@nBYU}U6{{p?n|xmkg8HD;Uc~vPcqXYo1=7ciBRNb$5+=wsnLcS)n%N! zWM-cUC1Pp5x(+c*s}V+*0GLG&VDK!}GlqJsGE_A9M7J!E3JAuc@}85`X3<;c3?sgM-Vf0fO6-;uEJ`dO!XpE3`as2Gw{^jXe%&+Fw*@$!*Fcni@>NG zn4+r^F%?OHX^1l1DY{iP@VX8SaP??&v2KQRMWYv-1+s2o0dfTBh)TTojzwj+4%w@@s#wHP8+=QYI}5u?#AFwr2okKAbnYc>eTIDu8GRw)yQp+rlvnWK zJ_=wLe*oz6DpH$W1Yk?iLbVQB3+3a2yHOX2PZ;{Z@RiHN8AXE^rgTq=0zx{zT^Eia=BrNY7#hcA)ZP!r(=Ky(up)h3OOIR*eOmpH;Y z7AzLWXtS6XklkDoLn2#;am1UibEDG(pxQ((AY>5?0lVIv8!X)*fSZnzJivRj_K@ZY zP1|EO%It}*Ar?aS_&qCgzng70nP9}@Ys>vCcHdmKt&Bu#Z86(2Mug+VUb-H9*_+sg z5MoB*4_Q&`c+S^zxT-vcRsTE#%sU*xefPRap7qn-0zi3-~QTd(d1(edCP?- z9dyE>Z~xH#Z}{+m2kx=OvOTugVxR42{$$0>Yj#=wSNpAa=bLxkZ?A24-fpu!R&Mgj zUAEcvYOuuXW1`vMVTpmPx*o~PVGJIOU^$B zea5N5{J|;NIf{w@3X8+y?Eb8ku%#B+Xf?B#4i`$rD4Ulu#t=(K-|_p{__F=wN7mhrFF$zit=`8*YFrC079182jor{uQM%DcWfv5#iWb2~IgQ3) z5nd6D#+P$s#Fru23(hjjn4W(bTMEIuPPN4ZW)#^4JH2KZyK*Ta01!%i3Z043M0Vmt zTHQ7laU$esmHiH{qknDk0Ei@j#s$~Ky9E3@mymnFrp zVq0{i3^l{N=tJUbwT2Xlh^Z}t##h(8FyHI|Lwu7LjIUAc18eSFd;jfzPpm}w@`4W* z6EsSZf>2_bJ!=*g!Hk-P3K>}_)!Y!B(nPcXj)j(& zoq9u7Ov`ALQ4q^ZX;n?xn!p%Up5om=J5sfgq!NF(bMRLiQ@4Pn7TQPyAX_&5}wE|7Y1U+3_I<3b-H2_FwJqbmZ(IOCC!CACZVJBgLNnJ`= zMg*9i912oxMq8iaiwwr1NEV7p)CtDRTtuLZWo(tQr#D$~xF_&-s}%#r-a|{#fXfKU zB6y3|N*V&-Rb-garK@uU5tHbWbjY5dGL8y$r7*9EYNwNEa>>&AXe$huaY*Im0$*fE z5=Iu{0ZhvXPuH4zdybOQn-_Oe7+n!qI%>_d8ekX+r8+%ZmX0ER1yIblbd^yHseqva z%c1}xAUhW;Ru{N{VWcagZjSK40#;jgUe=^y=moE;frlietH=SUJdPp*fGLcK1H;nX z*Q1+K;)7J6B(uzaLyNb8l==}lb1_N2NsfrmsctL z(zPqvg7{!ji58$s$>FAR>|oI=mD0LVM*WHrQvucA?<5PGu>xB5+9s$r3QI=0Mx7#8 zfGn+KEj%gF!gh+B>Ky`>e6@{QRYT&)J1vz3z>w@^Y}Ucp)y9|6npjD`#iUq6LNN*{ zPSGqH7h-CL8s(lql5rx@!5qF;*!l=4RA9c<3|6S7qYEI2Hg|djR30x%sq@uk%qvIw z2;1QhAr~#33qVJW(sT+NAWV_9WE@t2>|;&Jr7b((mQJ@TD+^=A7jR)~G%>_QKojZI zEtk|Idl=oaS1?#vMx3%3bwJhA^8gGxM-p-#-YARUO`%zox=`bVBFh5olu##tf;oyW za!46jTuoM+GETp!v8T210PH!n&HUZ|uR8bG_nm$8drm#n8BI`ZH*9reMte*C>} zc*|>Ew%c|yuiR;7ubnsBbJwk3v0}@YZMlix#a=ciFYedAW@ZpQbPpdyGqG9>5h*5~ znrzg1d_oV|?Vr-w{i;!!HgVlzv(I#n0_Km96CFs;NG&fk8Fop zR|KOt^#y012t%>^tVN&EaBq5F{PR*eiQrStwn|bVMd*gw!Bb z*fCy%3dM9-{_wlmJxWvy37Q2w|Nmx!m%5LF@%d0CBFC%|VOzPNaFaWOJ5f}kN~?138^?y)=*i5LP$`r`gvAnh+R z{@1_%Dhf&GC8?nZp!ggG(XAd`VA*vAVb6<;L=~ykB`_Avm!@B+POE1bEtjL_xU_nm z28?4F9d1X^f17l7(E3OxZUn@QlEv+SP`aV?f9(E$U{F`HP<%_L)Rsk8{Yq`(07^A4 z(k!&;P6N^U{}Xsd$Ho7mWCs(9T{Np|*5ViyPyvuyL+&+hOe#dF;>#X*+>%Uz;`xx$ z{V15}uoHJNc3prb4(tJ`q3&PeAXyfH0_h$H0PH5qA_8OUM$s*=fD5HnWv7%*Sh>V> zOBL4gO1k#`yZvPow^0E_bBATDPB&j*_L0ksh*8okDv2(*M7lj!2qkG%Re(^kgC&N5 zrEA3u;Z}rVh>@ZI;DS(e8Jo+P-3VAEjdCfeE`Vw!Vj*->5Nd=i!f-KJSu9Q~--2kx zhin$Y>WZdL%DmJ~wg!=<4ak+DdA9B{IaK=x}`IedNaas_SuoEs)SZ%G0ER`L&)N~OzsuRA#1^`A46%W?wT~f#j0OnYB zin*j~b+a_R#d*k=od^X+;mfX-v}tMu=V-aWD1_3yAn6XA z&k%oV(_i!SpND1&BMuiZqmI70Kw2xs5HZcSXeBC+DTvA5VXbVnk?BmEFAF=cHVcL0 zxI*g+VpJ9>2(KZ-3v!_h*gT~;;pu{m6{AaM;>w_CJ3W8}0ah)$ZY8P8nP#o52BaS- z<6_Yu1tE|f3rHA=#L+sA<18&vN#TuJMvG9!QVT*@6d;jy6aWt+ucQ<$F)E!bLRCPg zr3hh$0x1Ha6v2?Lg8A~ImsE<8mmNT4Uc?Z|t2o*UHai8kj3P63J_G1Bs-;_yii9aL zdvvnMGV)?9orx?e6#&*DsBB0z&?$1k1enf(Nf$MPQJJimED9uMk0 zEgMR7{oN>A$@Y$s<^HM^1${=Dv17U+ThBI{y^L-58TqwwOt52n%J!G-H9zve53yrI zx;XNN)&4;3b(k_8clOI*$nryLe}FIFciR>J@yqp>UU1Yd+ittfl9_F0X8wHNEsy@t zUmSbL+fn1=KKzbzj(88ge9Q;`;{ET~n;m8T%Rk%k^?UEI?{1s!v+L&g@@^})+HR{& ztUry6M`I4tJtrEBcf&N6O`$r6e# zVaI*0sNE2bl|sZKL`8GfuFx%wBw~96iUW z{n*zv-}%O8&O7;pBMaWC-sCs zb+qPA6b8GA6eFke1OtloP#TI?x_sD?Aj0nYDgvWuxOQ)1gSo^IPy5d35+sXIFbQ{~ zybMKP)#IA&3lxx1+8l!!^5@`{0VWy_6+gmvP#XjXjY51VT1J8d!Dz8RrHWQ|0R@pl zB4w1$C`GIC+5a4v`-@SwmCN z?TUF-xoYuWLs%P*#t|}>s*MTe!VV0>9!Ug_34)m~0BHpbG4^+?C?UleERv!t8va;J zFeNF27?maRC|3(>6o#;n*0Z(hFgYBpr>?_ok>?#>qusp5C%=1#o4W@sANAdX02qCe z!`QGuRMn&YL{h0c#Oi6X%Bw;=w?zP>D3u);Y?RGzY9`q1C`52}k-_LzW<+2iMp@=+ zc#Z~kOLEhpxKpc^^n}pujPAC`*bUT-%&U87c$Sde4eP>pj{^=?SDUmrR!Lr>8xlCl z(sN_Jz-8C1NEp8En`D$^bcF3FumEuJG?I%r)hU%! zu{gy9HY6;tjKhT&SVmp|jTVg;F*=sc)Wk_(DsO}pibZ!(5sHgwS`%@6DLwPUi_F-= zhINwyzN64Yl*hu0v~F4W0zjq&RtB(W9>#{FuyjewMW+y8Ud@rwlM+YK{0EEEOeHdk zMj*jj5DVoZ!jekitZ*!hioiwK5x?as!kC)LlDfjGs+9+>040%9S0oH107uW9M(YYP z;xN{gt}>dVc3@16Onrw7uXL??%4H(LYMTOIIzR_YWTU1pAUmNbwPR!fjnYYuVvfQn z;*53R%TX()q0-SA1<{hSFb+&Zy5S2BHYPGpk*=5$(anOR$dy-+W+4cq=VXUi5M7wk z(L)xT4q5$vyFg6#*2j=6jwu(lO()BRB_psQI#|=RV*V4xi^LrWal|JuBQ-_0YO_0G zgc8Kr$Wa*zR<@YA2L^zwn1X1>T!bRZ8x#qLN07aZ4Hf@rQ;~3$O1ZJtuQcq@wLFgT;oy)uBuT0*rKwq zk}T3m)IltiEY7HLP-Gdt^l3>QH6<~+#c~tWN?AcgX>%rJ#c6(d8{qE;BP-gd+5$C?MY;)Ntj!eOzQRc@Vx^2VS8=rdgPRdW8 zy8q#IH$VN*ou4`Pn3rubv;Af>J8m(v&yLFu`14mB^`W<)cEsPFdidWQ{r)$f{E@#r z^T>A{^P#sMa^U|u?4Un8_?`RiyUWa8yUy&n^RnG{TE6Rw&3D*#)2){;+jPn77W0e! zhZSOiXrN7Ge@Yl7Y`@h$GIojWo_p4*SR_h_@M4HsTvC@~8_H2;tsyQXj%gp(+tG}O zq!hd;BH*Xo2@6c_=qhpwU}w26EaS^nf>ww8UtnUSVLSCJ2sf3q}w+rjIY1Bcm=I z|BcwTm5kxyyojw=rU_z;;WC1Sv}+tM?$@*XZZd#ZvfHP&gN#8FM($v!F(OQam8+fT z**~>B_uFD+pM4X1?ypUK5ztVkhzC zXje>@3p(s7hKz{CUW?Bnoj61>xD5J(QpItiHh4|s4#$ZWu~RT3?lKO(EV3*jD}X{M zFlvxxc3zJ^LVQ7spuS5x+%N zXD-1kV8_UcC5{V=WgG%Cq)+>vDAL^lJ@b1o=(g))pZLu8dCu5hUY4}tB~_9YP_RWn zkbQT%z4b~}^AJ;*3IJgd$`pj^5H$TJ3m|e+(eMx(T3vSav#8%K zjD^Hs44HM4h3RfsOs%0JQ@9k9o!%Xjo!1auql=0-IRaNtF#{K^!WIUH1dv_LDiq2w zVi77A0f%oTu@GSKgK^==2+;o<2q;K+7>Xc_)tNiHZW(27l%-m(WFkp*YIYSrgAE%}Z5aLJ3Id;bC;s zVVbYVttS|ZQ5K_#uCSidbPHva^z39b7p*ep{@-c#KX#<;G~Z8a2-v>3)FNa3Zn zN|H`J#Z<$XI6GY#MmzmR+119_(y2r!VYx^la_Ke!D3yKsj1pog- z8S5}zid?d&c9aTF7srsZOCm@SMlnszM5JyMo(>;O%Z1V5<7J56;t1H%g)~a&FxB8m zyT|FC;^;lau@ev2Jq9!O$c-;!$M%l#WgE@jyGEJqBYQDhSBp*iacqB2N*iB(e8U~M zFs5ub88612=@gWi{m}<+du+Ww(=+==rtCXE^Ta(5J@5l$dHtFj9$Wvz)puR>$l9AP zJmUy_d57goR&GAC$M!RCef4&Syz6zxfAp_U{=|Xje)8R?9{Shk9R2PKPdNCLPwaox z2j6ng{`Jw99qhjR ziyk7CI3ObuN-$FxcE=;{&t_p>X8pVhaOkMm`5F9z`DiKZ!%$+61s*Rj-A$EzSU#3K9wb(AsOAusa z76AG6J~jpCrQ2e*Yiu*xb~C=*PBAY;7#`84Gx{N~wxsPKbHuxCIwRwJcX>*_@}R&d*ydNPy9Wp`|tezx_htRxcXLn`RRvNqs&vj zjNsrk(VPeleXz(ZQ6XFyS?-6dkxiFUOc^OA(P1qe-cWoQQ58fhXpv&0G`$aF^3m5Y{d zGiA@k9EHLkDRzxbmc1HSNCmV+A~edK7-eZ6HkWyA#5^x}LFm$1B1`8O^)XA)?RfPW ztDA*!_5_=|!}>h;`QMGehrCDDws_ckWc7L8$3C8JS!7g?BGqg)%u!dhe%*TGgMMdb zb&IL0Flrm6T1Lgg^99q2X6c&sP}qkE(wehy9NFh$KKI}KIkr)GCOinjZjY0|q# zgEdDMlL9bVEljs9b^z{>wXF=jn7S}VDZE<4yf{)?%)GK^DaCjO&=rKh5W%?<^|#>K z5xgffz5ph|t09pqdlZvzSt_O)xG)+Op^1z-D^-^rOt;h&-N2NAr|1gXXaVVj?)(`v zP=tb1b}Jyb2+fFH9QH+JB!VHjM8Hy~7qj_RiGoMXN&>TD?W%jKY4?G#A+2b+3@@id zXOYohYW84bUUs(;-l7Rh_e$;k_ue6O@7Gblgp#7CtQJ9?V=0;}0GLEv45>}qkaVO; zvG?4i&gr5ltoV=|1?f!oW}w0QU>1B9gEQ%DY}gD6fxC9 zD7+ZeAR~<6jC6KwaVRpDw2Ym}bO3WP)ROYz-Ykq7Qh?wT0-RJWz?e?)`F2d3V;MzI zKw>FiSojh~I&j^EEkMR#0JEdZ31tfwO zfx_1i_huT*`a8Gj)6=O+M*ZS*A7xlfk=4(Uy=BoASuU39(dNi#C<(w_5CLCEMi9u6 zo&G{nZBkJ9lA*{rq#;2DzS=B^>6a)B9n7Sx5AdSWk(!sZT-K8dUm}jye_lz^W!uWL z%LPAHKFf5WDrR20XfcdxR+4C9j=5N80jmmzqN@N?9tG$ULlQB9l~Irpr}$aswFO_; zT0O|@6p^(evrq(7Eun|3DwcT{0vBLLpQ1h_b2ke}dY5*=h3e#-=4!r8uwq0b%~5M9 zbh)HU#HDUXEl0-05DTw9SG6h;pJQ_o2Odl@WvK=&M;0x7IvC-|Ma+VP31FyDR!KnJ z;x`wq2VGmqwv{R4$t|Wl$nv1fp6I-T-EVxM#x|6D0h{GNe)nS*Z)Hc5XTQ1p)PuTq zljFkJvA433V%_meU#RrPH8(%J?$+$O&pdtC#`QNm_RtU3-FNNAbvNC6`<0JAaK~rP zJ8|o!GutnpS+V)d$}ML0-D#6|zG=_HKJeC)4u97-EUx4-N)Zy76tu6V>YnlGbSc<(w!Y!98iO28CAZy#KC^Rg6# zVn`5*!nAaD^A)gYCMY9;l+-#1i0b0MwuT9gj2(MJ8`bT*#u%{;WqcO5HG~w~K91+Y zpv)MtuHE4)zV%gq8H!-O{^k^;7qi8LKlzwX*iB|{W7-fSM_sxCC~6RYmSTV5;(z|q zXHPli(36h(h`+z=_p!Z+?Jr4P_1#PT-q+fDuJ-%b@#SY8S?l}Daa$x2d5p2fk+GWy zPNZ^{>C9QMOW-)>*hkJKf*i?p>A?UPVu?qyh^B>PpGsM@vba1W;KAsS?1J=~cZEse zibk5S7z78YiPxmyK~W);CxWSqe2w`QQ%2>*mnjNXp0K?TPPc13e($SSvVD1(AkK!o zmCXnct){4CQL*8R_o)GQL+g}nZ&a(isF@+mjxS5RPs_*xKqw-NO=K`)vhhLG6?0s3 z-)(*Y3>cEc^*CCh7J)7-7m>?IXMqe;(uNA6U}6+4jhqSsAiF7a5IZ+IE}b5OBafly zl;`gqKJ%0AaXrV#X=$MZeCitQTBvh z*FCXNFmwTR4bizYMCV1fP;n?QM)yaOmkKFFoa|aU5v{Ny3}I;2v*0D$CUbJJwo1 zM10rnH{W#qRqNN?Cy3f)A&~UqSFJWN)RaY*R6vVEQNN)uhDd8Ai1nm1w5i9o`za0k;30LkJ(+Vdp4WziKx*N|=%)>Z~zQo7oXxJvk@Pe;=m5;ONEXlNRL zs>8!5gHRwlz9Li}JEdsNR96J&3@Vz?`F8X>QN`f`Mv-C^0HgB20bnRBL}V!l9n4`O z6aZ(4mV4Qi$1W@+JE=%r81_ols?DfHgO|%F-*RzD0Jsck_4sylfJc_}041FUVR<2G ziIhPE5uBqqS}qiYRoe)g-B6>-YYKdGFLm7tTi9|bX)7QF%!n+4H8IUvN3&?71%T(( zL9Dji*(+etc3~x1%ta**tG4+zLG5s9zEv)G>43vmgVg{ts&YcnWgL~yQ%3N4N>eVQ zq2?lmP+Fpl!pZ`m23^J>x{UrSPbm0t(poW}qNGbqJ%WoM*EA75V-u_m!!BSY(G}2F zi5;(I*ZEoTQ+Iml3{9)0yFA6@C?J^Nko3CE1%Odo8KVYDFmum1V30xy>pX9v$}0sI zL;n6U@wkg4x51(H1g=eqENeX#&&tmq$B_|ayUBEv8BNyx<8OYX^;S0Ni_Es3?JxiO z*$uXpv1WWZq5FGM2yz?BvwURD%{H3tID0YssV8p7m!Ev}R%F>O^Mm)_^z0LBzx?@g zw_5TXUw+xLnVq+qdGo7R9D2~7pLW!HuKC(IpF8#M&p6_pD6^l={@8n8i!bl9)6BlR zFWGz7Wv|$A*^b*S#h16-bm^u`m%fDWFC)2FuuWt%mW~pljhG+>`@>|7qKe3%QC$k8 zA*~@B(>O96ql~d)j+iMJdo+_B$BYzH@Kan{7Z1fWg^G3Ra`Z=|`cunxmc5DX8@;G8 z!H4ZH`xY}JPObIkw(VwFBFh4*TyQaEkwSTD7S_AgFi5UlU`Dkm2~u9898D0?e-yyb zjY8V920)Nm1eD0Pxy@I>{)QBAeAy6-|02+(+tnqQGH#2pqOpXoMVWD4q}V7%tVM^} zdk33>O+#Y4wv6eJSTkPi1#Eab(ReYged19cv%d^4krW8BQ0(z#lsUf4!t1l=pZ1+^ z{KNUDA7g*{Q|BK0%`csI{gwacJOBKttN-;2ciep0x_hs2etI9<_m_YC=(?ZyOFIAa z2cpp*6cXi$-PC1?T{6ZfDF_iGMw#e>(Y0M@FcDrx3ZaM`pDKQ>I76^w*Ji$4C?azK zXe(Usex1M&aI^>&!gP?Dcn}>AiV6*uWN4N*&Ala_Z&Roq@ui#c**QCRU49TSf>Yx+4<+Y5C z6lHp9bsT5^>*M+VXzg>mrv)T~BJN@+C)Ot;<2W7S?ioPKMOZgVLeXWU025wAbS)_X$kC9b9C;L*I02oTa#TglevMhwV zypa)J6_87Ln<)#W__;__7RIWA5!UFKPA{PFqTsAWgOkI4}j4{rMX*~ z&T5s5067ZCOKLKrl_g_#RkcKhSa_+~qy}c5A{2mS?6a%vB8(K2L@1-eb`;8DsDOq5 zru7(t$+EDWSH|e_8a0oA6f!V$D{NAdjsm6_MnfG1HB^Q!qvBtbIPy@G?@s7y*RpC}*_hKF3~&FY_JlMQ9CQ$nFF-1XD>;IINwnxs29OquTIV zqaySXs#;T)l(E@C))g=>0Erl@4J^?G;8)(1`}r?q*?ZcUGMY>w_LuQxUt9jwPkcFg_D@G4$S5=2``9nU zmmhod#*G`U`O(w2KlX@sv+v!w_J_|tx&F(SUby+p%=VklynKt9t!y-JI`g`{wmIZI zZ#wOm_g;3%F<(6M!xx=+5Walk$KH0@5r6gf@BWiLSI+FS+YG+E$1a=hx?+c6hcvFv^THFb$maV3N4GZf)^0jve0EzznL0B zx)ELs7g4ptJ7{cs!7M1V{bf`a$z{ZUS%CZLY}>*3Z(w{`SIdG>qrkW}Fgkq7v4?r@ z8XYD8!nT_{7&@ga!k&NHvA)0T?@1l?vG<>M`VsiDzr6g7FP&|F8DD;2)wTHYgR6cp z`_1J?*8UV_n|y=VWfJRQL~uwJMoN|#rHjjDHA{b)anix9c#VL$ zXt|SobitQGBY?eVCJs0A!91%X0c2j@WZ(j`rYSL&2X|iBU(&)U=Unj)Yw_RdpLuYDKzL zb(_<5Cu%y308Q2`JQQhD+#Dg@CCzkhg8zfFjSp*bW@uU=t6r<&$s3Dlb!1yYliI12WD@D4Az;4IY%%t%+m@S;?|(M1JbaFZew_&F2E{Z*owgD z0Qrx;{&!LQ;uO4eS{R?GFrqcjT~roUlErbfs^W+stRvh1gp`Z1 z;JRw)kS!X@tN|)XG)ha+;w)@C(-yW0Xcn>v!jU4oPe{eIMB;NaYKd9_dR67ZC_CTq zT65%%EUU9PBIFwko;2$b1OPZ0@r{z^YbZO%EO>DpGHT$~tW2@*|^qDMtA3MJM%#ZGP^x<2d zc=(RhcVGYbhE<=t=!`9vEZJd;B`daEf-l=*e$5_RzW;#zjz8>xQ$G5RGY{MUykp;e z_EGOT`qBJ@T z)^Bmk8^s2bmr+1ph4#mnzy8HfoqY61KY8fE=bm=>7e90Izkcg8-}?FmzP}t__Wk9x zci%Ak%h(UE!IuZ$K_26e5g(Ti$udYKOY|pjzKpTsK|^5BsJ)I_1bS=W5-x?q}X;d)*Ok(l>H9a(+_X(m!#NjI%8goitNZHiLAk> z9Op%c=?c>&M(Jiz!GU$-fP#448c8&-{xp)=K}PGebQWH5Jps8ClYNMct&ChoVK^o% zqyB*ho+43Bx$tGvv0T=QP+GD6+riQ$)1wULrtjn0J;goFy)RbfquVRBEba@9vbaG2 zORb(}QDF+MR0Wu()~KOT?NG)O{ZzP8&W(2w?pUVB}j{4zV2eWff=Xnx{a1`tPdlL zIFvo0{-5x`U^k@Dyz(k(b7BcX&hFQF=fV~ebYBz)lJ%PW|cP84t5j4moIMM=Y3DAN&t zgc?aD5>_!Sf|hPng`}g?I+x38I0stxqUD~>9e_~Mfzts*ZX!Za#?jH0$TqYmB`|iW zn?;My$TH$o-T;i4EcpUke>9DY(^dT_!`!8spkUrV9nrBBaobYIW6C3T@}4P>cjdYii-yNseNU zm1Lx|C{K{^fjR0j0*s8+pqr7hP`H3)rwBqQoe%8N5sC^K;pM74NJd~?GfFo~7L}H2^aes&iYex)hjdZYfy62t_VN^`t8!yiuQN zTBEr`8mc?oS(14*XAGttquiTF(WY@&j0R|ZvS_WXd(F5!An5AgBj@q~{8+dD>zwODg}xu3{J zld)+`Sr^Qgmx&E!l-WDj_%cD3+iOOd4K4h=uSXuee*K24o_ylg2iINy;M$w+yzNTA zl7s|py=-R1R-3HcYN;?MAG8Oy_tabZjrPsEFHMix8H*kTN~A^WGm zgDIns_E-r80Nce(qm5{_P`RU-gqIy#?7J>6F0ooEgvB~BYhK>C_J@^w2OC*NpY0~& z#WtF8GFKG>ixkTu>j35)|iQxWj6s~>YStn7h{Ps5h5M=CFShO0C1_Qv^Pe0+v zFMalWa`eal&QE^%dr}vl^GRgc%h=!l_UC+m+56b|@|wG@-+2G+gD=NuqQG>FvW?vs z5X+!ET~?4VZ6hK_$fk?V5QPBSSR^b+u1ha_(fG2s`jDv+7J$Wt>GBK!3_v#|*P{=v z@;9XD8AYfn79=PF6=}kHkQy|K(GdC**Nb7nh?cK8+6Eol<$k`*T*@duC0Orc*J8(0 zL7DA7b;>!aJ-p6(j##7Di)e4qAn`Yctq4kjB(M_`dz z1)G`!-4-*9s?^qes}?3v`QXI>t+vS~x|Yi-wI&F-9ef!HJG|h{9lkr4f)&|B=ERp{k2kY4>Sc!lMaM^o& zRaKQR0_(zvqY`mOs18BZ(utZO^Kzu3vASg$F}bibM=l&?0Ui$|4Z&9la4<+ix`GSR z>X{Trc1NEGC8JSNCE4lRIaXu^6q&ITOjk^g7GVm)NQCVH7+_IRKtalGmHfVz!Wc(J z1+<1D!?#2%>?~4Ha<9&&)5?g$r8Q|rmCB2qh@ei6QX2|mG<%Lh!IYvIf$b}t6z3Qq{EtsDaoqR z9TX3HMKC&6MlQzblWiCP#;RozoLA&s7P`EGTLg6)g?CXQjrQL~`+>vlt>20ZGfkB8!;0K*F;aCFL^Zma$O)?uBhNAX5ZURYQ#7 z8R=C^A+0}4-9}YnS;VO}Mv5*YT@Ya{Z>v3xIo4q)&Jd~VQmn8z+^6SEQ#Qod6(n6? zi&hypJNft3x z&c%gbnxg3{sWXsW@Zn1zT`t4KMa85{t@TM4M`7A(t1!CJ3K+RY z4RTpbLg7mcarB=Lz*Qn33!#W3WA)Gr$j)mR^BO~?hAH5vWW;CT+eB2N+Da;sXoc#d zl2NFRHAgx?1yDG4VK$?DxsTKm`F}7$L{M{hahhFm*a`i)3ZMU5FrL% z^d4nq=~)iI*OTMRZ8XQ1@nSD#vm1Tlp;g)_GX+8R*Q0!4xeaAXe@qHlMxwFid4Esp zp@*+s_rUiadHBZF_g%I2{_F3)<7$uPXwl0yTejmio9(#uvTc?v*>>s7o-3EVZNFXK z^H=+R>V$)i{lJ@!JLJvhANRNRmrwfm+YdeXwFmyiD_-}im%aYg+wJ$iR{ZI{EB4xb zhn3rHx%sA>zJ%Y$_LeoVRf-Javly!_W)}RH9_5VW!VrWM5}m|wqnwNpOcatB!XF9U z#w!c~=yhdwTfLD@bQM3<6_e=VxxmW6VYw&(5`aj`B3Im&q5xj;SzVENRgyRb{=8%ZVpg+>|D@octu)- zrT{6sbuOlsMKmylDWeq*Z`6UX;}4Gu6Ypcko!hy#r+nS_zJ)dG+M6Z-mt1g$x4ysp zxeNT>mmmA`KDNInWq;XUl0ueI<{#ep%~iKw;qNc^KDHmn_AO?Y0Ae_JXje^^s1S}r zB1BOkE?|r(Gn$EV;-3BCwxSuLXlL2BTN&l$2&3$R z026v;vkNjLBgc^!*jy5N(+?&~?v+{)mW=YkGiIsfXhF=e2?`6w(mGH^DEN^DSR6wF z5-z$eDSc4^C^%nXD~Tc|VYG&wHX(IkxL8;L1!o~{^fFcnB)z35h&U95apB&wQ*XyQ&XnfV+`t{DR6`*S&o2eE?s4g@Ld|gj3L@1bY2|i84QJnU7ima>JA>oxV zu3~X2NtQ&wsgx@iU3QD3W-&Pm{6H!_p-3Fumg{oHpDB(QO~gC#gQ_Mx`N1_&<&Lcj4|ZG z*3c+bE-ne=NK`1xYmuAy)C;VsQH)N5(0}ZYBlxL`pZw@&KYp6>teAF= zt8$Aetc-@H(Ofv4-d&h4lzx0CmkBFR!m+=o0KB^ z_*FAwLT}<-{vla_6%3!a%VW*m(KrH|h)+}#gO&Fj0o@&(JP=VrDHKvZRJ%DU!yI;I2^hF@G)zOE@R+IVlfvVhMsl+ zDwUB0$k<%cCE_AWyIMB3#_n@ex~F|az+Oqu|44+c-DJENYbF$3KmUa_#$TY`mMSyYtG`w_Wk*n(MB-?9+R^eCAcVZnE<>Gh5HhynOl0 zuG=pC)7@A6?c3gP>JbN=eAxcl<39S1j~?{8BmVwPhyLB`|M#E2{0*<#5`Om;OZVJq z^F3B>y<)4)wp=o^{KbBId1eM{L+H>td<{v&=AL^mdhW$(qz`3_>Vg5|ZfG4sr^^L1 zjUdNBaY+^cN>niybWafcm!m8wp*VCf43TeL+!LN1ph`wJjF|9R0Rbp>dn<-c5r?3z z9JK-}k|LnIYU@uy3Bu0B5W6YUVGw-Wwl2F-cGHng0mKvp$x(1z+Z1F}Oc_~tSsX4+ zN>v;MXSAN|<)Yxf0?J4SQx%ui8NMMU!E51A`X0i2bOPbh{Mw9X71e>N5yx<={J^%>HrF5~bO)@WXokUl>> zazlzRj9eC#T%;2UK=3TpR(z8xnoph)1PqwS-joHIe(pgwYe)cX0LHvV3f<-v9ypqy z+lM6I@j=InK31$DkwqX}=mN?fvJX#fwHYl70N-FNeb~Y@%9qdq28Jas@fSuL$N?Mi z3#D6;P2U_HsVrKvX3wQ8QV>IsET+(Em_jn0i2BRYoy8$@4!Dm{#0(!m5I3w6vFln) zZ8H*c#HWn8RGaQ}6u3A=HYA8pBv=(rN7ciYB#Xr$EvfD?49ZLg!v6$RX^a z6^BvGrlwVoWyz~h9n*Z9aJF)R zVS}+(M!^|%EsnypWE4Cvrb+cg#6=APHWBs;W2pd&I3B~Cy)e_&U>BrSDlbe(M#v&i zAOYkOjf^Y;8Y0Rw8elpacxuU|BH0^alu>r)lB7mk0SoR-v#A|^VH=FQA)zWyTP2KR zh(%;W<>Hu&R{d2n+8o)@pD3ZnbxL~A{1JgDmA2>k06pF}1Qz|tD;6+qr;LDO3?e0c zN;L2$C5g;9WfSp>N(#?{eawqvD2cz&g+l$}wz^y}m!eGz3&|^9qbo8PcM2q(P;{HP z7M?}Skoa<|1S5hW(o$Hr^t|9DDss6NI%XfCasgu*0+U3<#?+?kKh-O#O0*-z$YSxc zr{Kj_Z`ED5KlI>Qsby*Icr@u`5MN$G5pV>jq_ZnNnO9+x5>^(`xMvyBoT3Ju)m?y2 zU>4U==f5Fcr#hvpgR-au8%P*IP~iUK-DqJMDMF1;36^w5*O&6fdN#q)R(XriL@W{G zn9+3ZL+1`mo;#*dCPu7+{p>$-Tmew)RZR38gg##*^k@NJc9x%8QkJ0n0C zmX=q9Mq5L=DI3Z@7M7h}t2Upp+Q7o9FeD{i-l0J1T4eYHSeG$-kw+Yu=A|ucN6JCz zxahKr4@s{Jta7tc+kiDHiyFiyUFFyn?vekiR5Fe2dx7e}Pe+df=YNR^PGVo*SNg;P#EHu6ugJZI7(J_N?PS zu+Pq$?zi{Wd#>Dk+oemlSu(TTCNq1l-1>kwzxKqB|J5mnzw`VP4?gdNgHHM6JI_4s zZ%_K<{s$ef_v`jvjxX=E^Je?(y4CJ0w%Ts_Qe^q1@nu{uj)t{Gjj=ZD4@Wb^h`4EC zqLY{%1*ydEutj=cUNJ#jk5Z6=iy*Qf5mk(m9SmtD0>W!iRG644MG8TH4bip05l=~j z^=;({X}q#hbA%U@9Uynn_$niAZc@bGlLBKjWE+=o2f&n#hBV6PO?5HN8eVF<+vv0D zo1jrcL<$xYB%_QLM0}R&5jjTO$|$ees=tLTtRY^a8Nz~bVXtaq%D$xRCS&j|CNM9yGF)7G7O6$K~QfnuVnk)vFE@ZhFxx? z$HMU;(`Cz!q4)&z#Oo4vZ3gf`MA2o0)Ll5_qVB>s)MuI&p0SUV91G|}gQ5nBBn7KB zT`;XOs<3LsOn4=NCq8`o;L_znDB|}yqvbg4z}fQ^YD888qwE0Li<3)uqebSWLPno# z@btb9OBaU#2$C0_rLwg!f(XTKrdk%riliF~R=PfWo1uct%gazoi@T|8LL#NQfmQ3n9gMkHT`BpFKl-)Iv8W_A`p7P-N@YCOvG`w0_vob@L_

;?K!AZxXhuIhlff$y1o7srYyvtY zL-^vl7{fZ<$klL45>nm*Vzw7Fmg5^5L87@u zqd*A;o;WSlDFO{dL@RWV$%ITaJZCbYZ1YTfk!DL;t&6EHiK!8BhN!ue@oX#4Y{SxK zZMv~PP?5~F)Mf?dP3CNP=Bq6Tj?SE%@s=H{`OJ8f&4B`omsSk9`%eD?_<#P}zm)IT z3>R}NgT^2;=dvrB<6oA+d2Hz%&|nKU1cbsCWp=W%{A64~F_vuWH5-67Sg$!29gOUF z$t7nnF)Q~y+ydcWr4t>SABUQ6aUph{PJ#7>UcjXJmRM+xL7}BsHR5OvoMq7243XJS z@-z>VfLYh)oZtxvY>lA0h{Sw=mNz}I}}L-E^`o?0#HR7MHy@z zNI(0`Go2ej-`K}9=Cu(8^?GYGQ4$J3!ThuVWpkS@QFu9xBN>#{a1hwowEblo#~$UU zKTr>$X`SjwfY3AnxAYS=MeNLo*jm&VWF;qTa^`9Ez&i5R9oQyPP1Xx!LV$>vMJrda zqq*uhk##y-j#gw-#aVsF2x&#o3T#VfKvAo$5r;Q(DdSCDT8d^7vLHhm@=)%n~Vjgn>~ys`117l(PEJYBd^YI)z%A z1`#(HHB+Z+HPa%3RopC1s!HJ*^M->_Z>xt2WpvAfbC{I==oH$kH#Ln4;kIJ;2JYa5 zb!7{oT@GbP*G zLdt?8+{S`_(HHdGTxvSfJ9?1j5UdRx|AC@<jqu}sM1(2 zX8S)Ttx70;9PCWTKvxqT3Y^+>9w;`GcDnRpcPF~$n%bi*n~$bJc$Z$gRumA%IpV&v zcG(Vpw3{V>bUXtxZX?kOXTtFykHh3{_;FG~+^x2sfAV`Xt!$=N4GgNvC{R3zOe84* zN@^FRc$$R4C2@JY^|Jt3eujd#^2jX35DS(U@m3H~q$=gm_e#McT_m+s&0Nq|M0%>0 zEg>li7G*;-^wk?kD6wi?$`VC!AXy&8bF_ebQbkpoX;(P$R>a%zgOrH2r}=)$KtwX6 zQg8i?4E1<~@0Ey<;+{fk^vuu0o~d1tLaI_ys$ig)-&Ci&1Jb><+w2+6MT9U=P{4e_ zq(%7OKFxt;&#QZ!b7Q||{kxyjv%}dvI-Pf6bGHf9 zs8zkXF)JK-9y@T-Gq{!cBv>MuzOXQ(OGt{RI9IaS8#uLUh>LgvrzilARs{RW(5~WM zL8B0~ISncFB}$PKeThl&6l7LBI9e^t7{Jz3P@N(&2uvermlnAJP zM5b4a1=shM5-DO2(4ic#Lsd#7Hm?TMAA1u!BWhSvAU_ zuFxSLKO-Lv#1(tkL3g$1DMdrz6ct&#kTt!#^xM4cLKxp|nF*cMDS_}8#<=9f9Ba$b zrl>rR|L7r`isnY^;P;2#=?u2x)?-IK4Hxq@gUL=}3qEHCpQ{GnFOL}Zz~c|!)vHTe zczMX60T13cXr?9dA%bd$QH?M)089$aPiyhm;suv@KVIbluJ@G7v zGW+=t@v;mtDGj=eufn_$v4*k)1d*gCzd@Qr>3LPu=#>L2x&#Q zMf0y;{3iu~6u4tR=JY9fGAbwlvWy~jsU_IVz!YKRuy6n$d1C%q?rnoew5<(Xvm}|X znKV3EM9g5&Bc?u^lhama#2H8oA-&1~&`gFXwlN=hO2K?4qBPlCufn-(GKjMZuw=?I z#NtTR5dJCb`3Y07>*yhtWMhVDvN+$63_t|qgb~H*U?f64tT=`lAgrY(Lx|hc?hp%~ znGu@Ie*5`ON`PAIKn_zWJQtA(I|uZ}Hd}?gr`5_Daj|oVf|+Ny;Y!;XJRmcJkV&JB zc+;*JRPeRxSmW@%oZ__HXw`$nqSZvrT4qqmkhlH?o1uhZiWgFPHf+maWHD~hmjN?G z5f>F(mt>?wUFnIqK!}*uv`Ei_Q9upjnV@7~Hk(V8rI4DhuCNDmv!7+4xB6NhTxpsv zfUJ2lNf`2iP~yHv!GC`?cOj0Zg5>Xs4YJbd>{x>JG&SJ?fYN<(nqKrc{wX1w+|Gx62P_*z6rMauhG30Wfh4t*@ExvTQR5$<%8EGEL*dQX zW_V(xjmp}w(HwcAEi_8oNl&1wnymLlc#}yWPD618sfVW;iCw=?A(?Sv;OqTpl(9e` zEO2_0tZXiUFoCOpj6ENkfDIk0kBHq;qIbL^d5aYN(GYJf3baToqBDBdfThE!$hME= zNfk2C84C@KGTR`IXM>R_%x}h{rfIJ@PVn!oVJ;iQ2kmC_iiFd@8tawjXjkSOoFGe8 z0C|ce8-jEUWjg@$&_pDI8A%sVLd&H&fg+h$beXwGijhU6Xlk17pt>LvI|RgFySmw) zTbU-fx6>l%2}DB~G5CQR=Uw%+H67=Kj6L$ln0_bx$X}v}P9dKX^Od&-(JKZaqY-h= zJ`Ov3LC1Y zPv%W5{_B=A20r?LEGSsifQT4f9KOB_9`Wqcyv9<5`@Z*Gy5S5A1Y!5a(S1PGQEHG@ z{v*iw?tI5~av5v$&j!hzDK;j!6jxk>t!<@z76tD$O(0g8PaZQ`XOv5;ZsTxk`l7Xskn$L1dJu5AxX?r7aRWi8XKNU5o7if!m#QvATw(6r0uC!20x3$ z@Fu@VUdg7$5El=7NTtTGlAf6#D5l5-UJf)iE#!)9 zuY#SO*%m3?_qBVABiaA@{$9X&&$hDd8m&vOzRUcR|+%YNM&_V0dnw@d4}@9U~=EjzSrRKL!# z{y&FdC>)8Gp}c@uE3c`TQ`H0|(KDx`iz=^=GHAQ~epyQ-VvAmkkC8|ZKfZx;z+X`NxDGz82%(7~*;fVp6MZB^o zSX*tRi!+-gA_e2%RyI|rU5z5jiX*5j;-1>BxCloutDe=tz*ec^F3)M)s1c*lJjPT2 z94vf_YXuI0Voy1>T`g+V9MbUCD@KLIPDP#))goZbeJ|_>RfF9AQIT2Ezriw>UO%9} z1L&Bx8E&R^$j84;^wEvm5zV?7=+(WOok+xkED(0BQl9manBl`#%x^Di%%KLbqWU*5fS?Yb2u3&&E$AYsfi zKLXY)$B8pqlJ2KC8>~pNY8zsLh#-T7&(91o&%83$ig=1-03vH)RV2kdWB=fUjieR? zCCY64#s*6jDNPzLtk*u+6vQQ3TYXoh->Ho%m>KvqZ!k?| zV9P=_@eQc-;G`4m#huA!B*oItKDf#@9RY2v~(u^+ou_~?zKADkP zheM5$j+EN9$XiX8S=`f4PHilyg@UmM7`p6-N+(D|j5)9>Pp*4HN~us|XZpBF27w(V zBn~e_d`vRg0D;uV>R>L);1ftsRtlqw83S%O+G-FZ*k(o$nP`40B}Us3JP4Z1f@0~BgBFb&Jvv@C((8QLWouS0L!3-J@kRjvf3h;- zToizmKH|PtwO7Q22oV8RdK2e#QthOXk6!bWJuFC9BvKWp5`w}<#0R=+SM^Z44E9oMdAU`6s!t7yUbZC}Hby;}xO51tBwVKfMqf!VeXnvq7Y)I|W zIunQmOVt>oX6TFKP@;I{0f3|W_35D?8(SOUZq%q*35nFLNkrHa^t;Yg3jlTco=&35Yr&y8cQF{mUU{?V$d?*1UeAUvX7L(VIMEU z&U-5SyQs~OGM}=*%j_)|GpDl)!2lvlJd8|05DIAQCkSKFw^9d0vrE}FoPb;^pqvH8 zwhTuz=Ajkf5n}>w$(blJaP*h%pd^rpML`CGLNn>M=J3Zc!kNv613?MeJM`|smt9)Y zM#i?2JT`FH*rs|*hJKM#&t_53cealNwWv;UnqY3sXpIVyydaLN3}HX1JzX+LQC1P= z`LdmVz&3bAE9k9?EMf2h-~dEW8<^J-g&bS~GcB5W4gWk}WDaytc(tHx_-Lbo8ffA| zkRlm+)C07%f1>xzW~vtI)GB?RU@0d$(~*jdcLy6ZJ;REkQarP( zR-mIg4acwySNH8}W-|=}IQqq6XME92^1%b?shXgO6qb~ef|>3J%`2Sye{+1Da2zAQ zU7I$Lg5GD&S>~8y5SXVVP{ea%0t6#QsD#<9a ziDM`-j8%aSn!L%b-FC~g0Nwa0a9*<#iJdWEkH|D-S=aVYe*Yh430o+c^EDRiwI-r( z>~L9HDV#gP3{G^Jtp-~PginhMaNTN;BJw9sJ&OgIDXM;?-sbH_4I3)rzCL|Nb4XH) z?!IeKc1V~L=351o!if>87H(Gk#SS3{+gv&naombT$g)uqd}@o06q&CZFu*HZ8PV7? z8&viM(fM$o!reP-RxYsF4*vqegdH61*reb~!wqYfg3SJhlmIx(vvrg1n(OH;tHbF^ z=5Z`!X0sERXky8vM`uI~4MFeFE;?z?(@c^jl2j>@9WaxR1?8pjV+r%WzWj{7=|mbu zX!?}46E9V_KchQ(bnQf$EjLzP#uYWDs02tR%qoethO(_{DzMm*1BB~;qHz#YHh_XY zIR6bk01L3HDc98I`uFRu>E~dxtvaq=chT5g`Qo!rsF0~ien#5RA%j_BoYYQ41J3%_ zUV}@v=^Il1DV5=Bf_?JQM<8fAtU8FsEJw%ua~3#PS*6T!G9XJic|WN-NAo@3{Z7rw zV`(sQqq9l}sF;F|Tl*J~sz5<4_<_Fr_3q`S4*#$KxaQGTog?3RMTPDbQR2$_rIQ)>($sEy%SPONH+bTkE# zG%d`iI;@I>zVu}96i=zp2{Y>TN{HN4ghCjWO$4cgB&rb|Pj8D1#Va&1PpO>7F`-#T zF^qDNIr2wAHqSIohH$QSA&HTsr?RP0o@5X)X`87ssT#sr7EOJbU5!l<1Jf%*PtL$$ z0NFg?&^0GAqzo4eUIiwb8O#sjGVIy5&ShWx%TV+FT^m7WU>Q>O)E0*G;>!rr0g)ZBiU~Kx~|W8T+ChCp8_xU?cS>W`3>uw z@Xx+qwzF+@HVX+bonWnp^^#H~N*6aNfeDN{7MfX|VZ%a$pruGx5HuEm!GKeA6!ca$ zb_h@za0)oHDkaLmBopyUHWWn6glI~)Wr{bp5EQH+3UVrF36N-+3))o#iczCPi$24^ zEBV1v=?;g7V9>hE(!KRc?br_IDngm3j1R?WD|0y8lIfzj+O303I9>^>AqW6OGf{}n zlUT3~p=+t1*oZX9iYQn+;I^vCM2pnoDZkrSxKY?+S|kyUh+tKR4HQuGFRQG2PG@oR zVcR;$-tejNyg6<#dl4>n^jgFx%sk5wbV6>)=5h8pvrn7d^yR)Us2N`7U-td-*x`=~ z)2ED`JpQ>^)5b4;Yr6j@wYh95P2RG$^c@!a!nicq8yyxSOPdAW4+d zPdO9Blaj;`NutadcXCQ8GDs|l$S-(RROeMfQAF`0Q;OPgg@46E1U1XIy&agsdd-ft zm|96a$+OZ3DcN{TP+DyskeMCGa%Kur2J0`wHM=FUS<9@;@SSuBLPe|>%&0V|M}`<$ zcGP81mNxQkv+8Az)yJyAEvd406ja<#^7PYOgl%y-izYr8+h_ zMY1`M4%^Zb~T9(P5?2SW;z-A;K_@mYO^npJ^lJ1;!p-GEu+_s>T~2$plg)4VPkMv9zf-A_kX1 z$EScjuh?B8>?Qf*rvT^KjZ~B<7)u1Dr7_H~hbu!>_u*OX=6pFt)Frc^jq+=+f_{=D z_F{p{$tD-iTXIlJ235W4g(W{xV9-JhrUhPO{$B!Dm;737f|3()d}6~?L(^D> zx`WK6pa}?w6C;N&{~goObYuX(TXcU5%S(2rlh1Dx^+%YG#t^QVa)}Wtc zPD3=%L`nk#M}Ztm`_Mm{&1))3Mhj%^c?EcBUURJR6z%1BLv6$O=2=wJLI?I z86z+k-9ujw%5Q15B}jVaw7|NkF(Mj5$DA!O8B$B`Z1N;_bBMR(NjJ1hT^QDI6N|X! zpq9N0qBF!)q+T^DE1M~TYWX;qCR?f`8i}eVD>a(^$hjnqQ|r2V3kEUu0Id$;Gyi-9 z>Oic6smw*1z@}x+2{u^BM2`@cmI^wEP7*bNME|iG;d`o!7P0-9m+VQVV=QC}#-R){ z^K1LZNnnT@#+=jkmP$}>nxL=IBtt=UK2N}{SqriQkoj#h?Nco(q-q6eo#qo77c=SU zDFe`2uj2&Zj_7bjdL%&pU;Xt@HoL3~w%iGXoJpS0WbjkOwkIF4h*x;5fsF<0 z3oF9tAZp#AcZh<7sgs853r}>Sb(kkN!oeg3W>KgMd+#4~h{6#{n@G+qm91N|bnWUT zo*UP#*s*Q>dk1_+;}-poWZ2_M$hFItF0#ikwQSYmn-qc~4Td6FLD}eY(b{$4eS2ii9BB{bNWb;g7cnD=h!fJ}BR0Gu|f0`_V zQpxNo9tPyE%DV@^i#-N!&Aga_X<*miy`8;n4xBP*@he7EG64$ z-o^**l`oflzs$K@dF$84eLFajH~L4+?#K3omn+Iw@7lV|OLbN?$inwz@ptMs3j z`?b35>ej&WpldI17<*vv#=Sb7)~n+=y}LATeO`T^PyCbnzW9QnKZc*#OkiWMZ_W7u zlH!asZy9LW4mI zer3Y*?)zFa@AW0~XKh@)xMKTyr?MG}OeW@##V%un{bmgktY4Pz7%F~>TclVHa>|n+ zF2&oV`X{Q< zkYJ{53d;?n1uBI8;78^kEXmT7O@73!+RPPCm`Fdm3YFF5gFzPK84VgJZmqXrZZ$V= zz{{RIq5Rj}qYdi$9?H4U(-cAT$L}A2JFrSt5DHreefh~2tEv$2DJ}zogdmQMa1yzR zqo5!wRCQ)5R)@AY@^^lV7L<%9wVG%=|r<7xB6-=3cXwibh214Es?^gq69g z18Q*9ScmKGGhg1d_5!w@WuO>L2AN-&`98Kcu^)KwZlC&W^z#ONN|;P$pi4 zJEse<2VWB6HtQh<3kDSd7)&_t1_LY3ru>v5 zgG!-_fB`_W9CAjY@Hspa%|e%(VL-;&G?Dq-vB|Rhom`@p!ydnU@2I1AOD!?Ndht!jk|2{N zO1CEZwHiMzA2khH3A;Ud)71rFwK5ZfBeCRAG{0D)=MEq32k(~_Ocnu`{9#+cXFZ?L2;E)XNp%$OA%UzEn_?Z z^MK2j>OI63ta;XWQw%LR)YmB>>=ws!>2e3~1k7Y@RJ*6iq?{Odl0ZP<4uz?sL$phK zyE$9A%t2Hr(up=0+++X*H0p)E1@LqAC{ zGHoIw4m~8<}|MH*z>wo^2t``w$ z5}gwGa;TtjLPqMh7Ok!nUgem>t{@#FwAKs387K^2fxif|E1&o{x^NYfDy@1s91heA zlfY0;EW0?Gx@JcpB$WaomM3*5pJsdfm#Hf7r-ua-<4y||B%n1V&No0Lh757RN-eXJ?O+zRg8okrV5cNcS0rQ^@>icGh#R? z2AijalyHemc$rv|ICabuH4~ZBS4XXUa}l|3xNO}{yAstdOqT+}-Nrpf)41gV#jvp7 zDgW*7>^NK2THfpKA5td2U-rz20KAM3S;NtP_nX&#|Hdmem%W(nV_yoSY=0SQ_I>Q| z@{)xQ_$Mh~8ESsxl?C3%UNrwsc=_2UZhG|oOBX(N<*wVTx!q=~?z!VSZ^Fx7$!?b! zUOsH!Ex&xgoJ029#P_j5X1{UXf9H?%U(CIUZHpOd29|9x??3lX_S@y7pWbD|jXu1_ zyJyc@ZN(K`&dOWg$A$qxR4CF6YfWN3Y>p18Su>md;9?LJSkq)pYwc%_Hvd{_m}N{K zZt`TnJ$f|37vLTZ2s+z-C2ZkG37qS8MShJ8tzWB%9_EYsdUpwJ%_U%yyX(FtWF@ zL1~8&G`{U-konXTj=KHEi$Ug{x83mbpPl;`-#+}jdB@pb_OUPJmg_F{e^U0By@~Dr zq~Hr7ZI@qYR0~LPJHKpM1qrQ(eYN@c+g~3 zNmwfxd6SS@8)@KGa9FD*`U&%pd=}|JORvQ0;>-$x4+`J%K}Sx~z??LKy1m2iT#(blV8}zH0=A>3}agU_OAQp$x>&Xm%2*@G*`Vuof zb;Nh4vZS}{bo+7&IN#`^hZ|}>xUEUP(1d`aln320(eBJ%T0pqyoG*NaMC8<1lOs~ zY+(t)s4Y#M6mf}86!5Y;EzH6teu`QW_`rXJv)~*Vc)AE2J~a(F7cN!P7CteF6$sC1 zFaV4_8E}iU_9p2$r1MD3Stfw*Y9dS_TimcvM>|c-Ijzo%?mQ`(vLs`=D~OWmw!jK! zoGcTBxkD>e!Wqe@hgw70?8!s=ji2-wjcoD9tN~V&WJ^tkODPe!lV$RPb6wGdHBrt7 z-Q|RpQH%hcdWl$~WaI_JxoC6F!ANd=1{Ej{q82XSUhSGvA#J8~jj z;?+@r`KjiiV2vVsG;W5@f{V-4ckaCdOk2}!8 z6WCyKno!xm&HZ+ZOBiCn}G>S_JA#MmRLI#mqx8H1nBG&{b%* zcVS_wg10naavMiqViqTwDB&$VvZU7Pkp7~^q?T~4Vi%MqOz}d{x(f#fZLiZ(M-#k! z`YFd6pLt(BQ~3Pd^ck29^=?PCEg1_J_+rF=z}CgUQe_ zTwO=dScecyy$IA#WX5*ZA%(pJT=;U)w|I5!Eu+uT$|I-tX`LlCQKNeoT;H(oa zIq&4#e|EY5le+8XYiuz;^~k+Yvn7G4&D3KuHSd_q9FBEV>jAS^^RL=`Y5_x1K`2^X zYnswf)2L0gl(*(FjgDy%jjyAtl11_^(pCkG5>pVwxJ1&Snb+K^AhK?{B%ov+ks*9) z$>`_~@ZbOIe}ZM4i(Z`eqbEy|X6md+f(4-^G(DaWqX~FeZ7x&%#* zc~{?)a4UhGw%^{O%gi3XwB!K)G10U_I%!j=ZAr-iR!9m*?a;=$Eq)(0wQ_^YEC^vte9EC| zw<&w0-e>l7#p+J@OjD~bf+wPLA{374q9cxMs4c49lf4h%9&0!`eLSH=M}`RSiVTje zVY`H$|Hw=`jKa}M51lxj@hoa8C^fnmEn-FF%^4n2VyJV;`Lg3iz=<|;+y+G?EwC_L zY~vVM_PVwD#hLGY*;Ce}b_hEwWNbj2*3I$;qC$Ji66=#-5e~vq(`(y3|98>3vh0v@ zUVyF$Yjri#q8?|kTnWJ`Gd#pg##uP&gcDrXTE#0y=Us{jC{ChPHziJ@*Z#k7$RQ>p z(HR)PLc@%JR%)FisgM@(Wh^E)Sd<)B3YwE76@)`fKFgkzy7sAM*WE)af0867&p5fq zbB0n*fahZ54CLPFChRPbQ8TS3259T9FW6hKqF`6+v?1~ig`j&QKLPhTH$c!s7w*Z7 zIgv#iddL$lC=nruu_7BJsp1kQkfoPLNrWk+IR_l(tr*dm4(V~fa*iy_+~ZpKIr}{) zF)EA!Ov}7!ysXu;N~hIe&L0YSB6dC0luMTw4RR-i5A?U@Y{?o}rW2Mi>I}1f2*3~l z%ScNOpJDftSlRX?Ys$Gw6@^k^gh(|j#tA?tpOX+~9?!BRE=|(K8Nu?9dz_IUVru6a zK_Mmq9cfk&gq8RN2xF!hvnG6;^cYAk4MsVpmBpI0W*QUGn501=tWK>~zG=lFMsr?? z*C-OHGfSXz)DYE?Btyc~>N0e7IHW=5G3iV>j=mHc206oYsnDcLBg%E1b(e;bW|77a z4b5e`>nofx<`ST!zW4@!S;kxOt4o7QX|pc!mH1VGwdC}adZ z86@42v>G1%8Oo28{UQK*g&K?v@?_k;LLUYa!h6v;egbbh*x=ETWHJQU3YbKjXd#hW z11W47cr=sLG4MhyTmwpTk`G6J?|Ysgfrs3njf_GjRY98$)tUzQA`b@-61{Xi;?x1Y0$)+yN&ZcPEAd`)nrs;nj}uDC@G&LF}q`^(~NKu8%_j9R-H1c zw01X3n|fORFcDLCL?s7CP-z~@b;Ox2Vu1~m>hqV>XIC0LCRAi6VE(tA!2*YU^2XHiy1mbEBIOy@EUgiC*^fkFJu4equ&XZNkw-hC@SVcy_?oj(CG``#CrTmhMZ^X;Zr~lDg+_nKHDX5D~Jv8`TQW74BQ7f>IE#@eziW-ogRJZ2eCcyH}R37 z3X%;zqXVH1Lzi$XuI(!g9G7w{LpM**0u2tgAiG}B}ME|@P00+OH>6tGN{nN(;95%F2- z%9~beHewXA!Za(Kp;>-PxL1-y$4{6Ss-}bt-LvLAo85<&29D4N{Ue+Cs);)#jIA@&ny^GE{&Mxzp4&8I!$w!}h!gtR; z?HFL$$FcpT)IGOeV}JS9>o33armKNvkhwL0Wq>)+gyqmQ?IExk(@L_5&s113XPMDV zTlfmXgxCy-)|~*3LOw}^CUktg%-760RO@2a)7sQ&mc-Fz=ply2iJuZ2hfU&$nv%(= zCXyavgAX_iHMwIt9VT2q-PL62rIoPRmJ$m{w+L$qwF%mf^8E7APln~eTgolnP0*Yq z0L9qNv8JLNQntK-o9`7CUoOcN%`CF(CZ<^K z(!fdD=wyy#EgN7rv|bJ^KjITZ(2!%DCd%=ZlBSE2U1DVTLkXtwfZt8kkOr20Cr9cm zSIE?>9Hu#iJVa!L1`d!>5SkEL3mM^1Uj?lYkW!M|lS8en5T=bw2r@YQOjSO_B%D`T zQ&KFljYFq=N`*2!!O@ohwLc#qo zIZ!l_ElEzP>9^`%xx37r%w2u!9~ zbc-7!B;n2}rY3qEI)iHYHy#j5k~EMpiU{MdDAM$+(Zb;^-_Qgw3%n7c3CF!cT6iKr z=JQXAqJV5 z;|bxB8E&8nbB0$^A#f{6C)#;$V0cL4(6Y^bpW$I_7$?StRx)+~&BA=6nJR_;f}++q z;WL*wneqwo$M-g^5-Dtv+yKL23rfOQ3`=WobGi>td9LNx+~$26B!r*tY3}*%t_Y`6 zESj7|HM&F2NT9$uqsbv!oml5i-$~Rn13+IPpp#Dwr}5B9sTZ~B8SLMijn6+69SGJ7PD)q#uv3%x;r648I)+E z0m`6_67beo!qlC@dO6gJ)>IerG)P)`BiW(W1UjTpFIPcX^(MB>Q%u@IKsr*TWGmsC zGC@xQ3~?h#!gbU-t;gYz@7%q$ev=;ZID9UdT%~X*&!U} zu}t@hmJp9AY%$ZX$N-Kr!UqC4g+Lo6TDgj&iGaf)mg(}Z=7!;&o~#d58A@{a>=2(7 zdhAwv!Cqhb8DfqGBaJO|k~vR4BAwN7s&%Z8)T%p+Lj#4QkSCK^YIV$Fj@3P9oFgNd ztC$%g$wR4&liC#0BM=v(0q$PZeFNc8>F}*}?_&eS0CIcEU^2Yy=Ll^mV?xT_#8xn0 zdH(6wmM&2HyztA5pZxVJ-oajIe;HWz$5Kn5eF$C#mVN0W!?y%(tH~aXS?_X`! zyO)2L4`r^j?6ND)USalf%iC5SkQs^u^k6r&0^h0aR@hyINg+N z0*7YPnO$I;9l$IznxOl#GJ1FWMJ z%K6DRexfV>0FVvn8=fn_x--i6bh=YXwZcNor9 z;9{89SG=HRc-aq=yKsZ}XSm{mSt_{FcpSVEM{R=3RB^nRnlEHN0$p`N6wxdiKemE2c9`vQ|Fk zFB6lM7_i)83BRcv-;~|*z$9)$w5Y?>#e}8ckZck(e~~0$-DL`Eax}XUCZ;>ul}Ld8 zF>fL|M9?rw-E*TRb^6IOp{5FFx}%ww2G+G&Vm0CN)0_)y;9H!FRMG0DAqiC3g>h(> zx9D{$4S1_W6FtpViNt6a%SEb$gXsZywL^t1`fK@%EZiK%0;2-7~b zi^Z$#bGDp(eak;*S=F#YLq69XE!fmbCc>d6s#1y%WRa~rREtg_NnjLRaENuv z^1r_LsbvL$nQ(a{DHScQOb1Sc(u^PkGK96tp@JZfNq}kQ2y@B#GHmR*c%8uO=w2`M z+OTJRu6^j$*ZgqQ?oKkV```ZUZv~qt^w6w!sL)TFD0szTD4^E>n1;tC*#}V#yf%Q` zf7}VZeq-C(6HEJIJ}cyYMIl3iO`Aii6jp@8ZGn4a2@dt@OpVkb^u?^8 zM;RPSXr{rH(rXy>lt99%l2SpDQKPeqZnTRU2R|QlSI`PS1?3i`uZZNqHBMOIXv$No zuR})bg*n_tXp)L@RgBd3kPsh-o;=K3trAs{F4|ahWAvQCbPTlWf~kwFb}6BU#WIsl zr&!*d?$(58g!P+2VgM+xMy+bw!De%}Tt-WgsMG9_!=7~rkWowoi&lLtDXC?P!q&so zwyIY$g}njdxt1SI_=T*Oem%FRjR~I9>})FhckRcNSVbCH9AOq9fjAE3L>H3u zV3Ox5TGQwlGlgzPDJhwCA)ik9H!yOE7*)g^>SY!%*lz#F zI1+^^duplEBwg0qS{;@~#G4k_$;&5si)~we1<2gQ*35GubpGS;$FOl@kyWk66?ub4 zkpX@>9cW$nZ_ahov+mN}P1Y4)sxjRJJu8)IC}VX>eM z3jLP<%d(`9R05LeDOaVaR&|FoXORr|(EQ|{9QH!E zglCk1E(9DZxui>m*wrON)no`~D30z5Nr%}Q0{T)o3Vuzx=OKB0byOF^z{~n#62R9B z-C3>ixF6(FNA zaSB9@qf10da5&`oUw54PYMuB$g_m8SUBWKRZcuNn?Jrkc5Pd50E&cA!%<9EOK?88eI z+`IUhJG_s*bm1-c-g3rxAA8rW-#3ky58izfkXe23?i)|O^tGd(GjFi}&Kvj(=EL^e z>4+5hW(Y&dxWm2ET6 zT4vehmR)Z7&1@fg57YbD5TEJN z${8O}g(8E8&?1lsal?kTXtlPg1igt_QbMu_GOQC09n(gG)gTlJnd${s!pkrzuWZGF zu23+H9JZwaOlDCN0)P$Mg2Vcf0gx%=USQnJ%N!=6hW zHi`HN_3P`QH@+{f5YxSgbO(yL%BYTrJSRgylNyr)B9(3abLp&k$6a*6sTZDq$|V<_ zUb*UubFaAU>@&~!@mG&H=$x}oIQz`wfB1uMU3Tf2nCG4I6W_*Wb0*~PD+3*%y6)%3-q7|1UAIR!CA}CjU}%$n$)TT4CKUR4sL)Kn;AvbLs0IoBI=XlH=#xD6 z-n-9s*$eZXI)=QO=o>_C%kDyy*sn1#{`Eip2OR~1?o83i2j&Da3X3_w2e$auQXHW# zyLFdXt(0>~mo&r0$~0bvi@k%52|?R>hLpXP?M-ZaXxj2pu@tl@ZDrEJ)gc6p-+p3? z6O%Y|j2clcL)gudm}w=KxQ7-9LI|B@QIt%%)f&H3-zXyHQ0)v#wJQ`&p`3TMGOCG8 zDXOn{iQ&*mKPEA?;t(@Wrn4G{|Je$~kY^OriQu-rwX(>BE}e_?Nhd%>n!UKtGCD9V^j z8mm1{X`D-%v&oZ`>#mHYDHBE=D;9?YtPk0ZcJ)K=MQSnzF2fo&?y8L zk#Q)Q0udK0=uT+Sm4B^e*r76UX66fEJVa~dwqA9Vw{lxk8c3$3vxcclst`^$f5_la z;^vclG!GpPoqXnuQ$S&!J4FBfw?2gH7mFYUY+|Ww2@MU|kAs!0z5V>w4yhlmPFgi7 zZ~Oun0l}=;K_pvg6J{ONwW>8y&K5Tm4)eiH4~N4{3yPDN0x^UN%}{Y>HIXFav?`*I zXTn;AtcwZS`pT0T>7ty@QA(-kaCD)EIT~;#Zp^%Z3RM{cMwGjaL1kyq*f1Cj31G@# za3eA>Jh#EoWuOneNNO@#FujJ2(+-_po)73AWNSQUj*>LPDWhm=_!-_jY2@QjtFPKE zkuaSNQO@XsLna?|(S+thOA-v}^WM9E7<&gg<){t*uG^N>shCoy_(YK@8i zrh-pqxP->ZblDJvk%p$H){LSxp{Mm@PKMAQ@v>{jb)+CB*U>w_j}0$-4clAU2%F3B zvH}{n;~ZWFpWAePC%o)e%)a{d>e44(eja3gc_U%vR1|6g9R@UG|P z-}1tUKci3R7jXr39 z+5a!wUw+%~eF1L16zZ));MA{meB;7o;SE6l3JOgCkN$wwY+@2ItpK5LpMQPz^3uVd z*|$AnpwCnbZ_)t#LZAU>=n`NAV=+Cm)ddg+7`&^iti;3_;z8`GX74>u@O$Phpfpba9x z@nIj~3k1lm_7NV>r5PgT$pAO-$I%KU22z>cZ&w`FYV+8>k z!^o$f`lDlh_-Dr*d(;`H9rKeD{({&I*IkT;k^7>TJ>^?&yz<(sF0zkoquI8y9q0C! zZ90$lv5!9L@M|tRi7{QvS#j{UaH<#{LnC9rIx`O1sWyzAzxeD4cV_J2}P zv*nNp%lu{XGU>E_v%o`RwhD^IG?(GvH|aM)Te_G5&D8iLN5_^l4hfS)bQv;X-;>>x z(3y3$tF>8GQ+lXPvpQ8|xS9Yae{wRKlZ8NlId;v|0>p1Z*V-*2ZF+G4)0|F*W3=gO z70MqAc(f)K&Cb@A{N10lP;9-Uq^&O8!;ishGhvQOmHvH$?Mf( zhH&OIQ7#ax7hlRXq`h-qW^8!TBX9!rkXPu;T*0!4Q)v7ohHmY|lT6@O(x6L4$4-#C zqWdViFnQ+)Cizr@vW69Y8z`fV){^J+%2kg6- z{jYbgG#fm(u=kD%%&wOo0+1Aox--a=ly}Hg;gm!2h@vB-$ODdIct}Das~d{K;gY~P zPdL0hHkSe9@G^{y*81x=UW2AFL1r7x11#&`;-r;{^{2IKt?1EYJhuS0s|XM|7lPum zG~#muUFOxYjc=TjhQI4o6ibP0jen>PZ&W4p@>mJQp<#-l_l~riH{)AsX-{Y zp_QfL%0AHgDcg6~%5BJwL{OCV3W%=p#}))i7@RJbCfNOVkyg#&^r zrQ&M5i5C&=WH}@}TJbUU?L4|&gz0ivEH&vRdliyQ3_Y~rljMw{i=P7qRq~lr*QAvO z4sg|lpd%khViI^0UIvOQ!Csj6AAbKgfB!c=c=gL)z6yW4+wf zUD^sCr?eOVfPgcuAe?u@%$>(!kL^0%=1tK`kW)GeR%S;Y>rC4>-DL9X_LJ=0mLn zrhDOQO<-IX9csc!A)U-og`h{%Xw8k(rkQ|_^{v-%HQ$DC@>Geg3mmQcFoDQ44R0-) zQYcuGGDC@tU58wyoDaFG)gj?5#xL;B-QYMJ2^ndrZ{5+!=z>##%pn~1BLzmCI~;b^ zB&n)b5vj@G*OWzd943ZEItk;GFRp1$N~)*3p!Fip`p||WKeFUIiteI~Onnti)UrZY zmpqRtG#705E>}<&vf9PvLVMfN=da;q-^W(N$hMWC=J9&AP3Q6ZWz2VaAKO>5y^=k% zx%^oB%Zs16Z^09H&VT5(*Oolt``8PfxY_ry7d(FLbwB;#j$6KWhb`W->o)J-bNjW! z%X`lGlkoCqcHL-Zi+SU|_vMe6Key*bU)X0;o6FwE_J2~7zm(c-V)RIsyr~veHOqjCo(7wCp+fKnko5C9 zP|>@}Ai1{_VNaM5Vg+U`wsp}8X`%s7tqx6ClynHULf>ebVO~va5yMX)2#kYICjxe6 z7s4g4)*ZngwV78qU@UDciUsZG*uxFo`PPOqd96ILrAUttBl{2;D}cG*Zu_j0moRZC zlsDRf$+`#u7REsX&T6WtB|vl2aj01n_b$Hhv}>=v;N+hi4ItZVo_G4OS6`{&$H)Kp zJDAETC;dRv`RATQQmZ$&!C?hJ29{xGv^TM%?J|SRUg3tF(Y}xUlVgwcmr`e*^8Nkx z-1-OKIl}LkZ7!d2(hvNE8D75b^7HP$@OkH}<+=5&nAr6Gwg?mMTSeIJfFvKf;xu?4+ z8#WY+(rPWV#Vf+5_+FiL$bZh%U1BYGsHGuo>XaZdM-t&skuHa253N`+vYUolWeF39 zWTiwU%$&?PcOnu9f0!edHjc3(Y64u6ZKZr7a0nBS5IX76Q5Su6vB-1*!pMG2Mur12 z!yK8;Mb4x%RqCM=(cM@_)|sG|m8|mH zP>dXCDNZdt+H+z=;+VwW4Texi5VMmO*3Xxy8?@{f*QcfAu<< zHkv_ZyE%bkYg8pTWBP73uL+j1z`#LtxiQo@7aD)W#f|O;T*ZtQ0>bIDkQT4RYSK}> zJQtDB6ohyS8GmTd7i|EPLw!3_V&?#wFrqP_)uDoDfT_u7RS+?yE{jRx)22koA*1MF zE9Hc_$IpBtkOSEwGyEYyf8i5T1gadSCS?qrm4qpmu2eCHz%K+!+@0LS=qZiv z7_<>1kpJaGt81SmZF)&HJvq--4v0~A9cjZvD+r$J;w-mTv_flj=g@g~V+CXk4|Rh9 z4bed8z;dIMA$ml?4IDlcHQ!1sqk1*Q(nEQxL?)~f(;PM-rnR?3xN3O-fcrUsx!WN0 z@rQ<@7z>sO*CroCQBGJdpE8Mq)`Xd)sN-XDTkVhrrTmn9z4X+O_{7x7qf?bv#CUFO3k|K{s|P?HM1U6-Zj1Tve&-Xre~tP2%XTyE8~UF41TPZ+ zlYjAwt+xqtbRQedI1>$jyn8bzOu-5*$2xb00XHx?=E~JnfKKa}3p5GPp{Z6ac7YrA zVl*BQdeN3x7Rlo@LNZrMyVW?+1{+KPemeVbmcii4@s&Aa%m^W1Jm3Tz!VzQ|qG~_4 zCaiYLQVb-&ceXMhqmWmphcFZLbckRV_{Bu116=ByQR}W(95SimIV)N#_yjaj7}&{f z+R&IRsueO6CYc*u2=NIk1Qh(~CZ153q4X!I;|QmYDJfBEuCkjjg$~IlIqH;XDl`H( zF{5?Q`de$tWq=_v2uhr<4l~M1+Nv{(){$}r5gjotj3!SHg-oY5;ao*0kS+2E%n@dX z1@6w%&8J?8sritAUgbQ2p_!85a6XK~BnjwNeskskoZnw%AIBg1wCGr!BCv_pV=B>WQoU9aEBN+D(rJQ?>=3 zDdUBA`rg-z&pqb%%P%Z?7-a63%ihQKee6f?yV3tiJ@eSDPd4)ZQw!!W@Zgu#f2ko@YHt&D$`zBxcT6We7 zE3LTv3bVbK?Hxbh*IR!!N$vdF$XIn{+tHK1&VfBaKmQRqd)Z|j+KPqm4%l~3t%{Gb zR)d*9B0LBwTh96C5&!4`7kjfBoCJscGRR->fZHTNUHHg%!aq*(L4o~usf2vF_DA2g(X2k{#P6Sb_KCKePdmkKxSv;_ zWJCG-YrH*fqx@9p*elt-3&$GXL(vSb_z`eKi<7NBHHkZdI zzkDD2u?KE@=L&5S)I~wZCRju%{4stQ6&h{y(?*qCne+*Z z7hQ6w(vfvCYE8OOVpUHVr?glq3y*eKLW#)6zM5ci+{&;NrG?|wTJ zuiGpudW}PBN66AngYI+^cBo_}A21P}R$^iq0+^$f!kiQrLeZqK-hDnFa+Pp>$C%_B zqx9<%6NfFnIScXACcXU8B$>{g!XM5{7y(RH%iCuNEWg zG^n|4kT{8S64peR4@Nc3ImsDfI`%CuFIrE7WwbK+wessPhLpXPO~!V!pP+igZK+uS zpRI$N!7VB*P_0)@_bnvC%RG@ENpro(7&mfQN=3&Xfe4Ma{B%ah3*Ru-L{c(kujF7B zqo%S%l5$0uQlXWG#)(>?fg#D%su!XW#dM|aB@;($j_DYaPBJoy&l#>#OB+>a9FjCE zjWkW24jdYW&iK-s&R(%$J2AnZ~V>}ob z4Tflk&Q~W_Hh98{f#J?f6(>uACWC|(&g=M2g+&8QYui$kqd8oR-UgvZ9{j+BA+b}K z34CWzP^{yt1tm*5YC@D5bgjBetb!m(41aXQp+V8HVRZwlQ7r%n=i^ zCcQAZC&LI$IfEu|csOx+m@O#S#xYUc5Z75ESF@5&bzN< zG{7(^R~Qm0Z8-WeXJTqFI2|-Qmyp7MCu87s3&QUfWNZKn>W1GmgfT=l`4eX!oAQ}2 z3Kf#@vbz}#bWVWA%yx5zbg@7&*c2SC@N%OrjH8QTuU3*&gDqOU+)J_tiGClQ^&m4n z9Mh4L@o^L#>A_@ws3sntrD5`=C09k*P4?lvFre^PsH^TB<#|A?P0AHLrlex7y^xyKcAnk$-x`uG??_o>f+7UP_F1O5?r=6_$1P$zrv(B1p_`4II3x*;=Lhm&C zN|<-K@oh=NlAt0c(7NLC@AB$0Y-@E5Q39A?DPRkMLX>bW4Gw`;_!Qp6aR}rhXtt9T zfkOc1RqXtFX$q3P;?j$JH_uznH(Yb2HATz zPDFT_&x!{Ek{|;J{Gx-AJp|B7Gef!{V0JPx<5>YH9h&xbw`UaawZja7%n)06qj z+|*oVUp$yREz8 zOxO51fEb1lWbms^uao7|%&M3e2h@r{@M~fv4qfgZWQ#?omUD;A()!=GRwPr zAixU3JU2%ZAdgSRBB7aG$SKr>PCsoeV`!k$5;W7g&`cpEIi}_YVGAm~D4`8OKMyIa zmD>OIfBlQB{*V9rUlDdmpsE%U-iTm@fT8PVh^`Brly^TYby_uXfC=)JjyOzE1ykU6 z5^c@vOOL+%L35i}@p4S~JWQvW7*A-XQ=S>c6g65cfvzLk^s+#r(6S0pN?6S(c?uPJ z%0p^n>caF0hap`Y;wQ#=v9P5(>jYn_j80#y)-rY6T~ ze3MrboT&;kfhOF#n5r}>i8XbaqtlN`iDejc$+V8*H5f`DCkw6UoTj2EO#VBRZE2Cu zPEY}A!Z$=|)2ktbDJ^VOMu3d+fhPs1Ra2{u+{Pz^CN_#-(c&c^M%GJo4#fg8E9lmN zATy#vUC@0e*{tnQVWM?lRyacj6G1H+pAYmbcIA4K;}4Xdc|!e?`;1@O`W1uA&5pT` z#m#UWqL+9d#vMSS+$g}|3H*Oalk!_nBRqkakw^T}!yMmUTf6`^GziP8jLE$0IaH)w zdK)~A9|BGmJ{g>Pb?#)oGt$XY6QHe33Q9HvF3*xyu*F;E4V#uUS{pO9N(cdkp21J2 zdLlEmt=YY+8x&_E)MSht*Q1NEYihJ|TX9i~qC!~I0_40)Ndl!yN|$;R=GiGMNf>^C zUJ1CeW?Fyq+h5^4;}w)wU;1zV`XANMhgBTU{f({_ol_}%LrBvIg#Vo{C(*fE^xQMf z2t!VdL+0=aIF;;Hm{Z2d?L^}cR{zR-3)1d#aGLzYU?vJ@ty3upA`+TI!IrMBpsp!= zt(qL7rGku7PSo-ihd^T^v9hY8L+72LG|;9D2Q)d$70cIF?=FO{1)R4Q1AzQwp*yn? ziEubZMQdZVJWHm;b_tVd%s9kP0Fy-~rVk0|MPaHCDbbyz6Q^WR)5Pu{x67qMjOEH0 zbMt8w2{xw73jSR%|H-zqZC)>ZZqbX+Fa8hz`@d7e@6SB>#G^ld@U@pb-|#tKZ`ymA ze}dqcRx-_^0i!l})RJIOsYmMU2=mkRWoY3;4f?vSFAi!O@<=%ksd0#xV5sK?3DMbRZy z$)_AM>zd@v-NwyFiX0NqN{Ofu(h{?AZvGAtUCXYdQM!tI@7n!4XbdvjUtYA}QG{PK`x`fSxmU8`WioHP zy!h8IJ@?x4&%E^PldnDh)Ehpd{o*q(EPO=y^~(!?_0rQzo_TP|g8N^8;qe!qeeluy zZ}`Ov^L-!trRN@e^nt7GFZ=!SQxE-g^9@(<=gWKSu=Y+{tiH|0t9^RA_54)Xm$7Xw zAGqsB_uX-Qzi8fhv-c_fnt7icKIG4r?LqIh<(hkKyRKbkzhypr|IK{??8^skdH82H zKIqdQ@jmuWn}1;2&DQe!#ioCIE1EQPZr@~d*uKuoD03SBMi6l4K@dX)egZ`z?oM^e&Z|T34_f3 zIL6!QB5Hq`GZezJ3IZ;cBA^@k2C)UmdC(gnA);fBfI}s^Cjgl2qB+AfZIsx1Wfz)8 zE@%uvUwQG_u=Dj-TzK(0r(SgSDOX%{7UGoSj`q*X zmtA<$cfNJ#-gCF;_sgf9_K z%x*3aQ-7C<=^m#U-+D=L9cjWL9Be_8uaHORVpY)k%B0Bz{WxAUwPN?MLbmnl0wk=U zsU?#nf&e~ktw09H;V9YCktAmn9IDh6O1Oc~TXde|n7g4WMeIdE1AZ`&dK6G;0Y;lbh5*kwqa#(+GNC)8IGSiCgP*WoXnZw3Kh?TB)TE1{ z;`7u5$Qxk3)VIU7TlzS&kwYyHZJ@i|u_82tzB-cEH|10*4h6z_;2od5jbqO=^m3UX zfN^4C#8IP%D%w&=iL{h(^faGN&Ih{jDX}@iQ=!saR;&3@UMx8P zLr~GjWPHXGM;8K=nEUkA%b|{~9GVa@HJPmt&3an(wT5Zh%|pKNTtO(9TrI+&~a0W686~L<<0Xjz#0cO+b?pMh2Q-r63i&h5)F; z)CAz8X*>lw)sYw^1xAIJ16hReT@J>EB+I9|K`7zlbl^z4R;R$=SI{^LF~haXVVod* zdKBkT{)KEkqb`R=g3wC7x~w)-8k2Fp$Gi->Jt*GG@bo2~3(13PPcQb!Wd~Bv{8P zEyW$aQz2m4Y^^(4?*8!w%OOmg0cJc%sq`WoQj2e>7)ZtsVKfdx-H1Zd++w??TI1-5 zAkcant(Uq)l{)k|EVlxbC9yZH_-Whx#~!u2ZQELT{NaZldGG;988o(M{lens7BBRN zbieq=zx_R#=bv2&TpJaJiidB$w&VlF{!QC0LQo?Ru69^N?#4!ucx9$Ab6Yk>(U;>`z=2n91GgM%2h@`{Xtp94&nqU>to^N7*m z&|H%`hY`~>-(+AqQVNxkKsp&MWTvUDRl1biz@amXgmZrADYV>919MuF9Ch6ZyltUB zyF3}1InbT0J5u+qZdtBb1??W#?J*9dJc{jOFxl^y+i?y=L(RRj9fJPNt4sdsmFIu4 zbm7a3=Ko^pGr#dS%`Y!}Y0+cw@-JR^`jy3xEqLs%1&`nH(sK_jUHHHgKfmeqrB6c8 z{(t$or*D7p*}Il3xcRaBF5Kc1E6&|+%>(xMi1)F#-*gRo$KJ=@ZHsmHoU=ZF3>Pa1 z=onHyWUq~U=?jkDZ>JBd(O|NlGatOiCzQ|a^U=@jxqjcr-g2Y&escZQKluI?S6^wF z)mK?z)fH9%mfK(c17x1HjE^aAx#`A0?#3H_^y2f+fssG<;XiS>(Z@INPr%n)@zbw< z>5FT=_dT0!{0TI?3_SXa6EA67ki(vKKVevlb}w@KlPQqd3*Ppf!9Cc}kD#ei>^S$U zXI~Gq4+}3ti{K;~2oX-SCb!Gn3)^is<3O4O6gX6q9cr&kbD;OG5d`kM^(G(K0+1o% z+itqy=midAhMl>A z)}2~9)j)T`B;jSk$Nk{X;bn+8$P5=l&G0hR9A1W_HQ`@)*2y-b?Jr+?{)yi>;()z& z-RwKx`oekhjy-SQagg%m7oC3dbr<{pWd&ZI{PXg2^M7gJK77Arwn^OdZ8_Ka!C{w4 z*O4hN0Wz*H7m?Y}sm;fmO zFTOHiE0zFMf)F0>Wu0l3cc7ZYF2b%#W3cHCRdpk zIo4hKeF*>C>zS30-g`*p=dLzzZQYWWc{Od^P$(a#i7k&ZI`s&i_+_6z;%WT#lO z0f1ISmS+p*KmPO+kDAR>)wD|wF&s+jf`c}_QbHTCp{*r{dLlUkliymGVGavf@=Lpj zWK4|YC00c4U2dCF)Y?WtSXbWS#)Re0kQtFPQIJCJ8AHm29I0i#O zLpCB|r9hTV=ck^29CibKAtoq^7$S%Vh1s$sKTs7oMj<3sF-{s0*?8wN{eKI4=mN$w^6@E{eVm%dKUSaL`7AJR4dizuY!dv?}OvDBEpG@vxHTn1e4I#c-;0Tcd#hVX2cY`ILV(=qhhg>b!)P!Q`Gl{N3OG?jQc~ z_h9njpa0xqlF;4E}tSAXHBF^;YSm5;-u6Ksv21K0xe15AN!%UDd9*7S%1wqyPWct-bZ7XWU*S8I9Aj9IdcLt+8bApm7>b#KM>{ z6HZ=}6V%Yn5Kcae>urlfl@QaV`@DE9^K3(0e#yo6+;RJpk3Iq#-*?xYciwu-FJ1xR zp{?I|3H;R;-H|-K1Rq|v`%J7`lN+oRoh9Fc_ul1R!E`>WyGGPp6{fh<-ca@GsQ?Wv zHJ=LytxJ6iO_L(<0fa~RglVIB0AUeztCXViUq3Lv3(@2FTG`=D^N)8jt);ra8 z$+x;x54UMh*d6|(rMFq8zn99f>}qwGhY_QrL#nc5^dZdv70kEJoh^Xe(#(BV}93Xth_efoOZs z-q4Qo+t-$Y%)fl$*;f}o{mT~?{PpWg{_535uRj0e%g;Uj`qHQTb=1?3-0{p~x4*c^ z_lJMJ@QFKLTJ#764KKg4qfwc|W@ z=N(pBeik&1KK0b`}SLG zh1)*n?J6rQcifMD;H~UO9=y-jx)1s60d}14zxxg|wTJ*TMs3%^w)F7C7tGM~_bRyU z7{6Qwb=%UlH5`ETd_YItZJI+Rz7K4#c_@(w^0XC^phCewE|^TSLm|tL7zrzAO$79^ z)y}BCBBB#IX0%oqS$@b9)8+hmKY@;6WE;vAG=Plnt!!XfD?$^(``CWGtQ_^tL;U~p z55M=-%Pu_S((_Nb;^H&Bl6}u@S3i93&HjJc_r9Kd=x(3Lw##hq$*gUbcSTsvSyVez zEcRL)w&H4*H#54dEEP0aRVtPfIQR)`8tX1ZG%+g&wPn1eyK}9JCcUUl6)RM=yd*ho zT@$rhBlI056FkS!Mad#b%GCk^A}C~qat;g=1q&%dtk4AEJV_u2aA0A|TmQ88=>a)R%rbfj%a49ABET)94# z&uw9HvXc_})`f4WQlqt!p(aF= zi|GuFKbHCq3G0i)ZwtBfH1m>F>N1+l$b_j?&|I+~B+nK;dODe06_+SPjTS^XwEFceG#CBaG# zz4r^RA;2YsLj~-DrxXYaP|^f^X$9^o;8c#5L$I4Nv7q>x(Yz4O&+3NnX+onHd@9GEVcyb3ip z63}9C%En!*LmW|;x?00}Xd#m!PXN(GfS4{?_0`2SC~4?XMh>&6j-$K5;39RERmKY0 zGWg#pooEA~)FC?iV}(4H#0@ISaVpccwANSMxFQ2YG_G*E;u6C&ZgCI{Wg^W2(P%iS zOOe{4H=W8JTG9-j9+jVe_E}e4ddY+L-UBZ`{J{Mn^O8l+!pQcQ{bKs%rAzE7Yl4@d zW{0j3MvaZOLOgYG&jA*uM}D4Q+Hn8pfB#Ps;`ZV(CP|s(t&tYK$a4ynvLWO*?bK@; z7nJbzEeYhsuBp~(C7Q@^sU}uNd8bo3x|5`Ki19O52{NN|Ko%T+tCf z0Zr1&C`0<98=nY=nB`~4BeI*%WN;KjRt8?i(U<2Gx+x8r9H!ZW7jN}UJ8GF+^{%V2V!`~sQb=zin;2gv-&^Iplezx>K`Pfg?H z*Ott;zx?uZk1l@de(z(yy5v#i`DgBP_}Kk7`9G-_pSySQQ@7rI%UL&Ee$1vHTYk=` zR^D^Rb#|Nc{_Qqd-MiL%ZSx`f%X@D(`JFPb3 zE4$2I%!Zo%c-c>w_t^debLV`}_r5;1?z`7m8fu^|y3fZ~rp zz*|kafYM@d2;1^p-_R~^+b<>~#?Z7t2!P3Ie`)P^%@a60vmdP&uq?N2E+bM%Pp@06 zfnrD*Kn^CKd-@3%oOu#{Xxe+)wwP^0U;We5ufOuF@BjH>zW4PP-#+}@Gmf#p?9Zfp z@@ulieC;Lo-gfO{58MGM``DN7V>@dW;Vu)Gh8Y~rvc_wf*P_va!Mdvn(VS?C*Gdwz z`O($X%-Gs2=2%WpNCV}xHFfIi(j$i1vVjBoQkV=vX?aE%Q%QgZ9Y^6$ZXn3eQ(r_J zSV?iH@I1gvl3ImtK_{E+PzIz)Mk|MvOzX}``BA_tY7tJTOTb&ux|Er60`U>nmpKlX z8BOq}jZ~@jxWFiaV!VEA@6YhdH@0|Axn9|&hbM|)ncym#X{mD^*;!s`_A<7WeYV{n zxA|`r&vRUEp2JwS`PQqr08a`7+FYVUDY0s|KJE#`cVfMXtpLkuu(SLxGk=XFC-4^! zX=9yZodw;|_`Kq$!b!H$U-m>@DHWRZMe|DRWXP-O@e^!mab>klpAz?Q?ny5JV*0B2 zKqsS46TX6zW8;tjnmj&9B{wF}c&5n-6_?W?G!`WYD6@bM@mZdkH_gkf51U3?8<=7c zfxBQ%b9A#$*asiDqe{W4yDqe8sukZP$O;(%0eB1#sx1qpr6LbPmNCIPiGyf7Pv=Ci zis(wUz$F8MN?ilxyO!7}bI@t>mo$a01bS z0&>H-0~Ax9Bu?>0uvnthnI7HQ&9RztNj*uLF);Hob^r=Ka@Y@D5#c7lN~=RCDNY~` z{tALYWFYg~;$^ui^F~prbDMI}x%A$;gy;^|B`{8$LrR>H#-}qyPZx*_M>Yh~SSoLV zl08FW6!{-TtD;Luz)#HCK}!^V7?IUDRr=#ONs^ znS@Uz7ED$UK&VusfoO*^Pv)Qg-9Jk8NhcqFz&?9<8JnNhd*T(oa4hW#htEY}Xf-PI zg?yE9%7V-xU-dYlPNF6QfMoPIzxc$sM4n4vHJ@|4hzc29ap=^byDK8xf22`1Bnne0 zGo_L6o5q7KnuraB!vub%rnG4_FkISWwQTu1y-uE9>rT^Mn9g{^VBCVnBD^KHn&xgz zbr}L3P7g4{&q*P`0j+duZQvmsVmd*M(rMH9LTh*sDG5R|T4YZXLYSHqH*>#ed-Uh` zdq3au%*zH3KX5Msw)L=1I2?A>+e&tqU`5mh38Pl^YA+Ug>H;sjy$~)OVU`?(F60d` zdnML~x8M9TG^As%nV%sJ!eyqkqYcY{12324%;_RO?2b(0(9l_9y`mMLp)}{-Tk9OC zP6-*2>ZPts_o95*WpM~+orY232)QzC2#raAezcP-V>pbaGmB}6^Gp{!^cF=l#L zH|(i34iyA92x}bzZ=$vGc`!4|s{-KwqD(S`_60d(QXuGB(UP2ooXj_Tg^a`Tu`j*I z$VT#Nhv`rLo$5MD6oITDCL@35BPT9Z7q44Wx3%bTH~atIxICiwXmPTS3?q9P+ZHpJ ztcH}^bPg|jN!yZ7#pK@QK%#Jnen#H(q(x=CWd!*{85U=2xD7#2vYyX}V4;N`70TKQ8OEaUswJ8!v$-!B8j{(l)>_7movHhpiu zQ-+uAAP1Ru+5G(o`_A^4{doE7hivz?gSYWkHsJjE{WtvdPHXM5_3C?V{~`asy!nP} z`TynF?|Rot%e`yGS+ka#wd}IX{IAVrc-fXKEWGJPpYYx_IC;v6$5|wQaLxCb)V%KE zt!Axjth(y4vmdGkes|tsj!o%r{^^%NT`P9q(*_@G2m5L2 za?32U((GCOLUfPab~|9-eL+7mfFb;7o!`sM2tX20RcnQmX$HoDP-1|mLYs|gtvCQC z4MApiLYvFbF}w`1aDJB^wsp((vt{Azw$t`Fd(GX&HndlF9=PXD@2~cM)6hytoKaYn z4-|@Ji%maCz`NEqlpV5Vf6Y7CT;&gR>>+?nH~-aWjm;k&1qHd2x5MrgBiNRcFt3P! zV$SmxBo`5j&@>L3W@3=IxCpZ)nZP?M#7YQZrOZ&Gi&`#+=&v2T|Bl~1@{99MJ{nSnm(Mx<|>@k8Bc$2%?O)<4u544(c_=jcE%uha{54giDZxSTXb)+wvm|ps}uE2}{AzQ0! z>SA;mx=6LUNY&Qx*7n(@1c$bKV8xnKYe0a0VmVe#Oqb-7bf_0UJDf&L!sI+vERs<; zHt@2iI(B>QIo4Ne5mAn&jkkh=CCM1`Ub?Wi3zG1G4COka`JZN1I82Eq08N1tgUsP& zAko{i1VTDO)7N3~f;5HH93Wu);u*H*-Aa-#PD|X>$g&|^SI`ax$P!1&| zKIICIz@36lP`S#8UiS3T($}h3 zP`W4qB8OxqkWmY!p7zT7inX0%|Weisxp$G%I z!OxsxJXdzPay17U5|Y~}h;;!!G;;VBe{@#^(k-tlRnbU8Bu7IQSZIZY)X;PqQiDn@ zP?>uYhE&Se5^WdwM`QpCoRQK84{oj|b;8ozI#=-erDBGN`ZJDwXG0oUWzl$-yjf7Y0ejt)iS+Q4~e|X%;UP z_m0q)$CjBeEHm+E!$2@RVrHD=7sI<2T^lMm4IW8%m{WSh)NUZDjt$R9)zU{8>a1p> zTa9uE138LCoW@bZKUQ+bgfKK3jXevLl?Pt#e4rrB0;t~!CzDo^j5%U3YYVeHP2?eK z@87|~HjdG!oj}8wNfQxQha+f57b1=tG2)5y< z+`ANU)Ea%URHk;Tl+vmt0duPvN99W~o|6RHBlM6Pqd$N<=NDTixxp6vMby>Kb!;D%FrHfqGm@GGbaO zN*|dTk|{0K$`FX3&Ll|OlGv`yF%HuKPnyU~=SRDv6%Z?MPU4j(!W}-ykbMtG#e|GF)u7lX0Ev@$+V8in8mGZ)~m6I zA;QRzvX6R!%+N8sjADy9u-u!=;bng{CD~bSbNQ)<=lR~(z{^iBzF%_sI=l=lFIjkp zgV_-D;sdjf4lEv+g}EdcX{s`vo~LTkFD2p z=z4O>dZ!J&s_f-uc`&)1Wn0fmXZ}^{Jx*nNulYlJZRUGld(K{cr>$1qcjs9L?77AE z@7-XHx4aHs9_O*g`^)}<*;exj$9@W^{L<%+_vY`9FS+pKFMdv(yy3cQO)-u(Qu$L43ANo!oXV z)MvpUN^jCZozSIiEJ)KQ;)5g3Xa^`{Q1$Zhv{z zH>{ZQz_La1gZuCEwl_^qgS&qEn2-5E65R{baLq;=u0sfHNT44%0a6N(zw_*E@Kd-C ze*sN?1n>&ycJF^L<-u6B>^^J9SF#}pwU|R~7~iMNfF_?o>qBJNPB`w99sdTR!4Mw6 z1|WmU^k=?`kTSg7#`FXZpK-dTF=&f#fpKo(Nt{aQ2_~QZmCv7c@(E|2{$(4=AhW~R z(D4-)p9?k1+isTWsJ81hm!EUvH5YvK#A6OUV5cvA_DJt9`~PJ~*~hVOx&BAaW8Zh@ zPwg-JKPmgm60{#Gd&x%l`_@RisS$`w%4z)JbdR*$Gr1vF&WJfMX^a#IkOGgRTIH* zs0UQEzf1y(ww#zHJS?M@<|w8XBBUcj6Q!Gzl>QDozo^kqoj#{2vHHUgeb}6 z!|21GGD`7lk<~KfXd0B`B1D4?12&~KL0f0beKyb>^uO)8Pkfc%@Wab0GJ(!4YDmau zMXf7EZzPb6NQhS0oB5>iw(97ZF(j>9Ar`18q%TPG{1vx%TWUVgyn=?8Iym z&M-^QZfr7$1%V!Co{WW;=$^kZqmuv~fQKL<{-Q*+sLeBt0e+Ton=P)KX<(oz$`*DR z8%7Ru8nH5*#!TMeLt~Qp$#kp58cjngQH+7%WqF#U4^{cZHT}Ed%n(;atU#A9zKx<5 zI=kRyiJAN+aVZd842(x2ElzY=(Lt1ppH>on6b!XRD=9Ptwb{o6&EO!rhrrYTyfOhL zs0uy`c|lU4V;K_KcEpAl0K}#UI^#p5%VNc=#!0tEsZ0rxx2;`f3c6eTqYF4+k0?cN zfJJE~k>`g(9V5*!5W2YKL$`b=)gw&KY2%8LTVfOc$kZJIEpZe%ZjA~G(4TJ9kWXz* z56wp)l%?HbW(5jWCHWFcLP*vpT$HOhpZ9{XDDw8}L#&9-V?VNZ!FlJO?Q7T+qq*_G z7CIXw3`b-#qv(uMk|&3>dbwWHK`7d-NZ;vk-{1D4v}6E;gTAZL=v+m)PNrLqvTRB+cvNo;1POCvS4Adq`6il2@$8+RE$V?^t44BI#Wq?NoG1f-PSZedi*5d*PBXF zTv^TGCY|BxY1ji)(NKxDDd38k+a;yyO4%~aj8eiM&nvH0PkE(jr3XztGp?tDy!EOT z%v_erTL5LEz{QaA-~8UK=Ym*kavmFew#6(HUIwD=EdTkhnV(wR{<805``(xLmw)^G zV;#nRZpnk*Uj~``*q6=a#Sh);dtZnzJo_NL{O|+UEWH2nd3Rkf`+aYD-#b^>Yp1pM zn!VOe?^_KV9(ehn-8MdO*NuQ>kQrEZ02@XQL{DCG-exTz+A(ZAl7E=8zdUD$)%>5- z!Fz6Y@ILQ@mwoSR`u*jBmphNW%k1qAJNQ5wuTZBCeEIjN*Dk;O4%=+y-?8M7k zS6$f&ZSMv5{x4AMlVSch%HrmQP5AEk&m8mix2?MUHd}&tFdfJVK10gZK;T-`?Pw@E zR0#J$fbz&ephrWt^U@@7m)0dJGl+-g8*(FsGhUijx+=i=y2Spk9O!9@UIG6 zH0?w*=*k?s$v{MZHO1<~4?WNca|lK2FfXuAALKGi|UTE;vvCyGb9CZQ&<8< zJQ@IPhrJnYS6V|hrU7#XvfBS-5lbr$X^$H}!(F+~mNCeD#%U+{(bXl%eT5bZEF$m~4!fgjlJi^m^v{#jq^dtX1k=zF*R^fF-i?pv3S9}L6$*h&pc`iJAE%#`?C|R1|M&m-PdGsmiwWEck7$CbBnNW@2>^~N5#eQ{ z#SbqU#iK4vCy#+Xw56C9K5A((_4tH*>XBK$ z5Ga%Wl))*l77s`$u1;w92!JLSC}~9%P5V5c*`bsV5wit@9e?H%LO8XlA!(Ncx9Z~2 zic&;HZe=o1bIhk62@FvMKgoz0!Y;p8`1BBrCTR3v@-*=%5d?acfo}pZ9&{BYDwPy@ zu6azObE_hca!(=^<5AtMyi(VmbTatWa!Z2v1dqBJg$)DU7)s2^r8zUd={p&Hty|ki zW+?_HGd2&j1%|GAzd6(+YB9OIx}r@ttAm0Y58bT=~%Evcy28c!<>s$w_lXCrvGMy&xWT)ouX>2Fw_QFmqJyYvIg0JWmrimpri=ws&050)Ivu=mr|(FPc2{;~=@<-kUr<-C=@fYlX`&X(S5$}Maa$Gr2u*9+ zHA=9W!YUdM%w%2N$6ki#3adnY$7&RA!HHttVO9klzggx%#*GoNp#-8iHut*bRaZo% ze$Pa#X`&i1d3yxL92Gp#vr=H>l~-;Yv93~_ZV}b3qN_Y<#seqA)NbWb@Y_)`R8(oa z8DPyy6R;n$3?U34n;cAs~AY*Z&kl+CAaHA$Pmp@g+P#^J9qk0sPBWY-vsiuiiOWl0#R?=?z z;L)6^VTF`0EhP=J7VD;jxJ69QRCPP-tw^I7aPpphw@T|`6s4*lHViN@oQCvAvQ z#P;avMncRoB+%Rh&od-TF)NjZ;X|1ulOSmez4gIWUK5&2NYzJ0M5U^$m2jCMEjnf? zdh+F8nb=NYbgD|E>qiv`dGY*8}ZTFR_xt!Axbc$&_LeFWQE%zX#j z<}&#F_kZhuO9lUb#>+r-Tg=|B@MltvWcxoU?=Qdb^do=pvhyz=_vZ2+UU>3_XCAh> z9A5TkQofJv>WkTkFFwBjUS9m*b&oH)ZsEO`Y`euOTW-*QRgX}!^ zF7Nd(%(Hgcay7}0vlo_uWuVw5av%7Dpq}hI4ffdnZN86v_`dIj zm$!NMy72N_SAN}VU-hcjEx-J$XOf-ez_RQ6x7@<3%C?g2FWXc07tG>S=n?{Tbl4Zb z{57+ETdx2+-u%TAj&oGf{<2fe_I5jm>{VquT);2bD0KH%#Ru%WC+KG-bSRWGC!B50 zLVSp-IGGF-1ISP_3OP1Xy~Yb3!jI5pFj)qWgexT(oDwJvazd1l0-#}mwX^I!YrD)o zk?pl>&Dlx!FVeoidB}nLo%DqhQ2d>=U2=Qn-o*xfQQ+man6&~XL*KT=(HY_j;M-ZY zP3&~E9|!CvqwlrH&fqHW4%ianQ)YCqS`26WlO#b28jvnm5MhqRjW~Lt;*vPlq|OeIf1R}U`_K|zHp{-O&EJqvP?7*FeoY%af?VMBF^YCZfl4rbxBTmlG7(Poa8I@{FRYtB9?Q-AS1p-lrhu& zpp73$m&um!X<*Q3)bEg*A1(}F&=U9sPgJ28n5kX+t)B0629`a_|I2^+58fBH#ca$7 zgy^6aB%(#~3oNZXYN9HJD$#jnVfU8)s2f)1^3s%S;RPY#_GkXdB)%VY`J?hGd54XM@BoGc$@N8a2W}U@?*|z~;?mi-32rkNMa~xQmTy(OgDb zBL4kLdKYfXI)8leU@A z;BjSL2I8NjnU8A)S2cmXuft<;QC&ty^F~-;8k-bjhsETrOGU_fe^A+VJB>SB{DQx>=JoHAi8P}VxhgjFij%HRBT7+V63A?03P z2Aq8~+XGyRf+6^f=kI^_%Rl_;8K~K*Y~TCx|I7B5C11(*ee9_WrW(eLeKEE3Uo#^joj}_WG+Y2Q2$O_5r(ZxaSV*+F$m)FCY8bbDIrp8N z^2<1n9c2E(XOH!XY;P<3GPVun<3ByYeRFo*$!@aumwgcXEvvlYwXa&vv26$%gHy*g zmwo0M=yR4BtgydqPYT{dbPU_qv}`)td$pl#gLQ@1yv85G+P8w2?U@40&K`@3p+#+~ z4hfP25_(Y=eX@mm;d$s_GB682VsKR*SeCTgMy;)0Mb3~rC@#Uv{z;1Rl7Fo5E{sn} zh|1Pkb4`?W*IXT@_E%YRcH7zCNjp6bz!?cl=?J@nX5ecZRf{T~)Q3#fvw zYpy=Q;D9h-3rPXO3`DU9%_tVZ%l<{n-%D*WYZkFistaiSAmH6kyV8vE?)GYL@yYSm z`l*40Y~dotas?+GeBeF=dgEJ*+~U^*4&ek_YyzUYjjKe~_qU;g@6jy>z!C!Koov5@j*7k>AW zADj_h{@HEUdul!X=tH)c!8G`ypxR7n?g$QL#>6p+JQjN(npq>oqtu*|H?34D$)l*H z2t(6QQTajacKRyDSgO=5Vu35MF%ypB`5FA7n}ib`EW)E{u+C?!{9AFTO~eRyuorrT zC2hs30=|L0Zgrx>6-q&Ab*&HNpul&a8Ng!eR@md|Zy@<;C6z!<6*bDb0w-csM{y+? zD3iy`Z;J4wwuo9=HA*XJhy?nRPkDKn@DWXTfEtFYk>gX?)-n8q%a92NQI95?U@VKc zg=BI>bUy#(WObZc6x0dT)M6d^e(Z7_+7S`|!)~J)k*ZQ2rUOn2xGfg?=YRS~%J8JC zQbo+smWn*g5NZ&cw-tf6V6u+@aCJWdK-u8)fBu($Z`SO#^Y&h9@FKRp6HRe1TXe|Bm> zQ37$g=6RZPtVDklp4V|SXF@?Kb2wZ-GI=D-BgKHHAxB7F^2>lU@7{ZWQ=ulUC|`y? z@~&kl!|JXygkk0JX9&(7@ziFWDvUyOO(te3N{@1mRT?ozk*PX4s?dik+{*LPc+e*q zQAPSdWPlQ61pdHKZDN8&_9dOrhNgY{O9t{CTa%cfFbGaUgq{Gd09|k|JQXx+*mG&% zNEhe}b4Mm;a-&}0Pp(!JzlnxHcc4fz=!D==OOmf2c|c-ai62V3VEBr${4gR(#?*XTNvfxs^fbKTTt6x}0bJpp{>`L>xHRb6q!zjc7mg^Mq>z9x($>@mK41<~* z%E+XiE6!jw{(8TzXCQCXU%Hs>%w#e;S1?wRS;`6dZLqnu_?THlS+#NzX%;9&WadnA z2v3?7Pf^1rEQv>^Nu;a@*0qQ_da*G|M7J2G)1Z&|8KuE635I75?Ke(zgz6B4h+4*+ z)i17=AgiCeelfrn%)8G_H1_leQJ8H>14r^|HTIO^sTL;~75vGx9&PgZ&{e6m3x(}U zt+$t6bnX?G{Sain^`>in_571hJ-!Gi{{3&ADKf46TIkIq)5l!%pr4>-qr%{6fol9T z3z)_hrPhk31FbMH$E;yjv#I&Q11J~}<<(_K68I{7F}&&)QAPHa&vv(}4CXQ@lSwy& zsv5?wh*goNBu=+dO(?B(73<}S62*ijh0;>(s+-JeRZ(6ONgMiOBdVlsLh#^}_^OdG zOv}TWQ{9@tjI(ypr^OPkl%6JR^E#cif=#0`>f#{^g>aM-Vv<#nh>AZtp6o4CyIFxV zovD;{We72PSy6K-U-f)a6Zs+G=-s~Zn&ffCU(yQZoIrSOkuw;U%3#2zN_3fOB>}Of zWweEuK)2H-Bc^H_KP+p{u2- z_0ZJ8fYOo(2un_1hnHR5+HQ^#YDNU8OMiOt`QQKYX`jfp%lzwS7Qgu11S$LEm(As0 zJ-rAzcKX_9u_gZ}j9(&$Bmn^#P(s!)+>TS1Jd6#Y8HfQ@a z>@WM?*WTN0uY@G`KBC}V&5L;G%Qiy2<_{_>n1 zH`;o$wg1xl%i(1Z`1{}cjxA=d&e~}Ng+Z+hGJ*iRn4;%i>zJT@Nt%dYUU zSCU71$v88fH|9zqs@VvtaT3~K<(Hc}Dc zL`gLgm*IUN8pih>GZdT7Hqg)f*69E# zeNgP6fxvFzWqD8+_|=4PU8$XM&=|Z0gx&h+x6NNx*hAlEuia1}SvFDwy!CT|FFrYf zj&6HS;Ef0%^9e*F`1g|n`QC$O4z=4lcj($*S)tI!0dISGeq>O}MVz2HP56|P-Dfe3N?F?8Ab+>ue{_u7#XEcez{(J{^?g<`rQ*g{h>ql z-|_TQjywB|lkG2Gc=K82sF@cgNOPoM z+a!|EN4(^js0fe8pSWFWD)oqr8NVctN9j^=;}D20kD*3o+#)tB5m72_s(KQ7_-ZKm zI48l|43rSQ(m#GG1^^A+7=(E$<}TNq zfG87DCI(JG=K*r$i-a{nG081~yyQxyoQz*`ONW7dprPmtd9qie4=Lr;o; zIKT>nBok;-7w+!pFSdcL&^eszxY6_p{&2Nq5c0Fpo-s)iDYhz%zl=It48)Jh*I00uJ#2Z8jepEpb8WsQ0p=1)#q$%HEke4W?2oH-4 z4=IZIMeXT?f>S9gNHC8klW67yd&rb~NAmMUcv%M_LaeU;Q@l)hniusMA#4ors;g55 zI!4r@-q8x8CUm7Mx{P+yYG7@2oLYA!N@;8u7+$9^pMkE!A-Ri-WEfu2xXl&AW1a}s zAG6XN#A#p<%X`%&IS;$7A-{I2%Z-|=6vKr7Wmr^(EA*+b$v^u^Cy{KK69PLIeSv>FN z8?OA>T{p@Iww>pE=1FJXeDmZv8?1(Rix2&2ViwQ@k!fz_T`d%racn*aW_^R@nPBe7 zAmX90p90J&CbU42#!(!?InAA}ZV}neQ5XWeoLL7cE#avnhUck4D~V*xZV9QPJYs#P z2oFjLu@=!OCZW(o#q^X(2gEorB%CYMayzVu!7XA5gQWG6ZY352(+I?aPMSf2qWVyH zC^JMUDyiPyx0C9f@JG=KIW?yWPDBFps#X+5bd~3Xj7^83Va+MULjvX~^65;bBu{5V zoch$2ggA4o+tD2QQi^r^%4-r-Tq@y2$rg>KjDA^)ZQSA~%5BUt>EL$cr+M;ERR%*Q zR8f8@9&98mMMs$yJN%EiM1dTbEuz7eLF<^1%%ao^CNI;oPX><#&@473C3Ch{mCaU2 z+55|Xe97a+!%md$Fv+7z;pIuuub%N2%rDOTtCWv@!OOpX_7Uf?y}~T>o97;O0Nb|m z!}D&o#r*ph9-VAAFL~g($LB75;9C33k34wQ;|s2`zr5pCZ}xvu&SUSs?OO2i?pv+x z``A0a`|Vy;ZgY93_q-Kkb}HK$>^7I-Wk|WbW}D9Tmp^sb4o4lb#i9FbwBK&)?z7W7 z;N{L^FY|j};bjM~PdnvguK?R(hCRK`4D@~e_|G_u?Eo_b>iy+4-u7lizK`t%XPcxL z98QLF0Y-7ApdH1&szcg<9+)WsjM7Xz|7u^)7F2_x_K4N;PA=RB146#yc6oT&W-HtZ zD%wkJ?-hjv{GoD?nb_idP+f*VMeQ&9x)*>C&U^WqGLB)}H1P^HwL#qV)?OR&-~;yc zkIdkbU0t`%R0G8zJbm0k#BhXLr?0*83`z(5vrhC5QUSAc-hZDt_MrVr&~F0Jz3|>9 zGJO(@0t?H7%yx~P*|u@K`8#L1Wwj>Q$TVBnnt+{cZ9DZ%A1AK4i1~a%PmL1dyJ6mM zMdVwKf{Pu=wu$TyqIU5Z774xGD@LlNd{i_iR=?kIO4E_b~)uspKu=g+oyfO zUoiVh_66she#>>2J#hDp@bbKSZh@B{Uvxjn;OQs)^SC!7%oh)Ik33hiL#ANQ)!cFG zAz0)f_Ld63JT&pkV~8!fPyvPH9-!u(jN8Pzs)a|YQLU&b#Z%oV+7+#NC?{W$Oj?x@ zd&n@SChP_41*hnJ+TGA{c$fswZb1%vjR8O77*3W2wyPzvg8761UqGKK_(_9jHD|>@ z$867t=tL^#7L@`3xQefY6Tq|IbtnrC`KCWMllt^LqP>g;Z2&5 zEKe~R7}GB;e(Lc`zU@&P9O^O(03=g&(AkPY!|Mp-11vEBU{4``> z`v@M3Sk0xjr+Xm+Sk|;3*{DYiw|;cOrX#dP2mD-$Vwah;qmTNq>4S|xnzNm673mRT zX^Y=j;sMp!$QD<Y06uAM1yN$E;y^){0VKW>RCo-DvSh?0QVgD4F?-M} zMHCT9*yQ7hztZTKd#))$DTuO{3Mq@?+RagfO;LlNy#|SHr8DK3NmY6Mg2>g}&P-$8 zRbqP;$HXx(2;l@iVY`YlC?ZDf3Umd=!ei}fl;*6KP>fwR8dW$Y3UiAh(KR+<2`6~w zswiLBhR$C4qqvSlvV>DA0>_!0p7Q8${nq}OD4Lq$c+DOH4*rj?#;sWBy$CJ?8LD>;c$-Ac>V#t({8@^f-M z*f4N4FHA0>Y@Qk6E{eROF}W}g9}aq>!ma8U3?vy9yCq!Su+bNizJMsrbaRkcw-zO% zuE}n$HSrBa{X()L{D?A+<%qca*l2OKN~>y|OU5}Dahjpx=^%s?WqB|G&Zd%w+V{S5 z>i+xe`r!}lci|7d`|yIf0&LJ2c0TjlU;X}B-?;L}7eLLAEWG#eM;{b;3%MC?Y%qlK ztf00<$y(vcK(;UzbfPo-ZcX!tQ!+{^NplLz(AnA|8g#l_#z->=RiDh4nMtG-LV{yj zLDZaUJ|DL^7Ew~S&dQ~u7NW#mwcO@LX>gCbv z!RVt%X_}xfGielamQsmhwM*1sPXBRDNqTA#KYE;Hh?1#;h?102fKP@$a~d|x(OHKl zw~Tg|%4jZ8nKSB=MhK(~t4i`IFxtg0Pxz=lBoHFGjR(UkQ#{k0TC6V;*sXROGu^)O zstH7FBe{*}N`6hJPeqjuQEp4qDhZYZ zl4V(QZ7FjFmL0(ExV7K4`ra3!FMuI}&t^0tyezo|mVf*6C*Wn*-}$!o&zIO%_D@p& zPf7+}wwpZp+vSCK_)95x`H^|Ibsl@syz3sg>#}?A_~9cDT=vfOSK4j6)%V$TJ?F9a z-f?{Z8D94O^7ikV0A%nPQihl9FZUL+-DEtyybLcx%{G@mcF@)*9JSk#2frI$K5)(k zzV~&|-tXOGhs`!#|E;U6xZ*3nj~!lyi|x94PuZz#Y0G!N%Z9SAN%y&I$FpaxzmCJq z*!*V`aO&%|LQ`9xKp!v*w?fdsrp+LyqpgHsFWd)`z=pPmgvTObK`A&1Z^DV^$~~pZ&xqk36*f<&ZMS>?C$zIlK&3dy9GU zcg*LW_M;zu>!i;e2`~FUsWZRvMc?~!9@`aazWs(P9-4d0y?5N;FQuHveroaLaI)vV zCtZO-^F`jQDLnAtlL`(|LZE?Dk9(2L*Evsb_tZnAZ7qy$>9*1z%>%ssMK)a!UPvqEv>$x&tcYcCQJTUyM3v|Gn+iI%-aH?baz& z5|x>i#ux^_{J@pW$L}qBXRUcgEe!>}cu9A7xd2ebV`7s4tk5so)p7PyjMCos z_Y(wu$!G9^R)d_eG=V_za}i5@Ow8Qrry*+i4b5*vRsc;awKDW!fxZCXmm=>nIO&1N zK<024ftC@>=B9<3b(#aKbt|!$DTAU@FdJrC^x>YDu zz)_T2)7Id0)hP@yNc1?jV4I#+T*UJd2~snK?Ko8-idR+f`@l#- zN7hk*{xpZ&jbn-lN7>FI(I%CY;z6;%B4#6^R4KzVKhv0#2V7Z-jnfqIurzK_3S|i- zT%%F}d1BFPi*r=`vowb;W{zHiPT?sSZT^A>05q6rq=DChSMXF&I(Q`?*6T`6cp1pz z#=L5FaK0a}j2V4ls2IAuYOqO44M@XGlHq3+C3LA?s2`i`#w4D}>i6;{6pQL~cJUYo z@>OPXOQS;Y!(B=_1Op=GhN2^~#+A_}TrAvDcOqfxY|j`EG-+a{BsxmU%SY^r4V?*t zGh5JAkwV10+V2d&Y@P>T!^<6puOqP0kn;MmQ6bL+lM9_8l$X++)q4Lg;bk4?QYDY|_kk-gI9b`bxY^j#AyKbIbcN#P`0eW(=$^g0~E`zV%ScMz;5;Qj{w~xGEX| zD7@k7)_SX=$z@8$ql;0bNng**M4~)dqeng#O1u*4-U+aLi2m}V1<#j}J7F&{uH3F3ygfiBy z30R&#&zf&)SeJSXnJE?=Ub1xiwN{wt0U^Q>=XkDp%-wwB<@@ck(+=Bhe$H8^J#gPG zOCG&{-u<`UaQ%-izVN$efA3VpYkqR!L-THb`pNmf`1#^rK0o=~Uv@Fy;L)O{Ez5Ms zSmrCTrU~{KVhy3cv1GiZITQE+ga5Je=^I-u)-gr-b2u<>^9+HApoF{(*Qrig#I8J? za6Bp^qLivmc&S^ex|OeMPTVhj5UR_D8k)kO2>#17gODrO=c{dEFzeZzt14Os2>gwo38oJg1Hy(vBO$hZ?=;EU-o@$?=O2vdEeQyoX6g4 z+YJF^hp~O{O9Gt1WIN7~vP=iF?KR7|`rg+uhiv=d1KxSa2WB0-_pFcX|Gpy*nZ4ic zTfJ-6+gDlPFTTI*tI_>+lnglR^Iw~9x{-I8y}WGS*^A8HUw+SKn<~2QCs)`2hACEE zd1a6gSO)jtIOmPwM_W;_GcX7XOJZY7BA5w=_;9o^Lr5Hd=vZ|S#uXxlA6?bO19c*T zlde!FI%1p4z&LmtQq~svlEc{jpA?1RWuN@=kIzSa$+5#59!@ux23@hwsku{$VR(MDX1&Dy1?G(dPegr`AyMYBCwD+YiA-y-X z!30Ek&Y|S{-t#V+z`}qA1IdJ!$>bS^!y5LWVKHVpacaMQ-L=+oc)P#oVl}+rR|wnX zkho_M=it=D^;cfJ^}9D(b;aerc-+y>UHkuK|H%B)t1f|;Z774u_LL<^xqq9w;GD17 zV)p;bA30?AV~;t+|1W!O*`G=I3ugZ+6<)souA3g7cejsY!^?tjk4+D_9`0fQ$&~5g zZl)-bWTML#OL#0wrWH=lN>{b;OjnV|U=EseBe_K+N61k_)pd(FV46oO`%>McnUTc>bR0M$`Q(o;S5v9hea|J?Ks{!z9eei>fH+dBq zD)_yt3oP4~g>wNh`^8oar-&UMfDi>m28l6)M<#5G(@%Cd^=w|?FAE|KWQc(~`Y1EKj zW3|;F*3CBE^aFeD*Esyo(Odo8bZXqpoMz)v0X zu{XW(jX=7i;NI+@uqm_moZaL#M-5%6Yq*OVWHQ#m!t=JvrG5<2G`{6dIZ zW}4dE5=J)KrNASnSs6qf;zmIrVB$IJ2`GL5-^3>ZLLrt9WQiW#Vk@HxW;{~yzfz%& zP)+Ewn|Hgi@|-QEKBRn9ouS zY-EvZksb!OOdv`v7P-okpW);nVvuBbyD;7@2Gu2si52-%8Vb}eJV_v^3$jVD7&K%9 z5g-EA{(8a;Uqe;kRnIgxPqS%)-w2tM%8PtN zL5e10>E-kMtkAk7BQZgq!c3!H!z>~?Vw@->7&aK>CCozE{^aoS>%_Jb)_NY_MKBH{*+7& zbVQ>?EsJ(Ga)l0ZWw;rHo@dxAgHjP@=A%ftF`wvk)vf9(8WlRXXj84yU{(<)HWW#I zaE6!lqh*JqB%Tq^_<671j~U$v(fioj z)5_1DPA4u}aQ8`H`qUvGoO9{LXWe=GHTTWE>9$+0dhmhU7S6xxmYc4)^78XxXSYu* znfKf?i$G=;Win-GXn~@n1&8vA=!VV;`q(H5_*+BFCi-)i8PSCDQ-JsN>;Yl3=&8s* zIn6a>(KRCI$FbPRA)gQq#K09zw?=Qyry*sOa=xp)t{D^pry1NTr8eC{$GlBv)>&Y! z$ubfXY7sv*Ff&J^40k(H&T)%bk!wYoC}F5|`bZ5A1BT{V>N;&W0}0XH+M;AdT^p0G z>i(O5{dfQSzxtC?*<{kK)k(%Jf$`)ht&=(E3Bf}>^psRO(oj1&n$AWH@~$KV3^5Gr zXiIGZnctGqZMNgg1Q`;lwan{i9b2`gZHX8qW$^r^gkjW`ahpKbp~GlpK;87LBH~~` z8A2kumJ-X`_)B>nosIcfzLZwU$0h=_Kw0#xc$OxMVJjYrSQnZ`EDk{tF@nj^u_V(b za(EfVA?->#unjLeA_6aad0Co)%#-uj@G_)qi`oA#JALgBmw)r~g)ct8$ji%inZ3?D zf9{RXJa*4ZFD&qV?5i&Q+WmK&zyBVy;N_jRe#;))`3vR=K;CQH^?mPa&imJgoxQU> zl3RG$8_hoU)qhCIBRZQ6GE0Z=zv;nyzXM)A;=rvxcG%7b%z6K&8?N?OzV`(z`%5OE zGQ4az*+-=P?~)DW-FKetv(jR6c-bB_1p3vJzXU%D(La9V;lLy0D*^{4@CaFf!Om2R z*hSjnb1y0?vZp3~_S2$6#a2tGxov1*6DQ<|Vk_B3m?|K!D;SFo%Y>;x15i)7#a2#_J?BZ zOhGgsJ^WxF2e!9sJ=Z8b+d%_qmC_dS@{TrXKvu|?D?krGfIi;hwAaQK&jl14#Dv=! zWRBn;NP&?FVH966pHCPo52T3v?I5#`94Ff$5no<%%H7M+{6KANN{{}?2mSitfc=+_ zKgQ>;LFVs#{bc*gUR8EZyFZy4co}N`!P#H+ee7?Ydi*Dk+W+`Ze#rN}Y%WV6vuo$E z;bq^)hL;`57K97@%mp(=4B)9JSaNID2$?1U-b^ZdLdPQ?e<1;i35CC;Gzw9o2$Qi$ ztZ2d2Eh4sMCZ+LfLW+M7-6Gad44M$12w%xWH|z1(T6Gv+clYauO@EPzE;jvrC(20h zD+fOt*!E%7^^~_1cp5vg-_}1I2Z}m+%?jp}2=~NyP{EHIdYo3km+ppnAu{-j>rk}i z)=1E&9Yyf-i1XouXe)srnVAy z#s<5RMmDFd_fa9Gt(RkXEa=CsNqU{(hO9BgBV7EJRTy2CkELz4u|NMZM{+LyPvI{I# z6tNU^$s@8gHtJ~NpEbr>JDy)#OsWmN=uln&}I5zL;1wTQ6rNAGx9Z}zep>hRtPz3}Lp)V9g&Pcnlcu677AQ%kd{MNxx9GjzEKVNru+DAp z^A%#Zyoack(6=C2Ms)&5%4sLQF|Qh$6(43?m2GMyD8fhMXm$0xEy-i8ir1333 zCtqJ6j?N`KX3}VwWLM5-Q4a_T_vG=u1ejOu+=o4nCZi= zZe^G$MRc1qRa7TOs&tGf_Lht*MK~$xopBrY-rw{(WD2AnwIyPcJhauiig+Xq#XL$Q zs;Hsiy}_n#GG(ZzIikon4BQTx_|b8?4r8YcL|z9FkLZK4dC3KSiN@CG4 zUnbv5E?K**c&%wIQ`Rd* z7B_VB+B43=h{1m@2a2I%c}WI9_6jq|3_G{Kj7L%Dv4LgZ$Ci8?TY{H?Wgo|W=F!P+ zvj3C%<JG+k0~e8eaCjFXyrCFMs^-T@KxM z_LiHi_tuqP^Ow$Jf8*3sd=bkF%l3{z%Yx4%4nNc%F0cNURkz*B3zBbq^BZ5k^A6kk zQ)A2mT|97y=oHof_XNYfI4#Nr04;`KuTWM38-voosfYsTg9q)bfy}~h08p@P2U!)L zeFhI4L zVCE-8KLXHsuibaSql!&r2Tg%l`;7k42|BioYnvDvwJ8iTi|{D`Ak#C1B{C1r!$<8e z1GCUBGi^-6LB8n4G_ClFz(WBYFmaj`O0wHd6n>ju4kJ0g?W8xmSm$uKQf=`SXiQ)Z zQ41LHa|9=o=}k|*`pQY4$F{<9%e~<>%e`yXEMNWdy)VbJ!DOeh?JS@D?XUR`7>sPA z8O7E!Kn*Wndcil(KI6pCede&wAN!H-pZS%`FFwl_bN?shdtc-Kq~PTx3-9w>6HfQ| z{xgD@K`tX+8X=Q8C5a)6=c`gkk2uX{exZmWz$k3Z#{`nld`w!zQB-LG8-dAmtI7yq zGW4n44HZqU{`?Gm7b|85TcT#3cDa#;T%qC7q>H6MihsK zf!u;KnTVp7idd&ei#dcGdHmFn7Y`5;PeQO2{D_5;!^0AR=unRY2Zf$G{RT8+t%&0INWsBB{MdGF9YQGtQtgHjEz~!THT@<5L%)=p#eJ6wn>~+*L)M zuP-cCqnpfkRg6i4S*h_LiL_ZgF=y)Ne)*z45f0lMJm}e8a44Wcam_hJNAjwO;u-+1 z27~5|kjWsQ`Qb%|lF=365&txP5UrflF3GF7%oGCU&uQ23s*QgCbPO_crcy|j|MAeA z6$VE3%2Qr%3btvUE&YHHgMJy1d7xcHk|i<`-AaamMuXL^=+g;jRcKNv%Krjh<|yk1 z*YO6oK)!rsVAxqkRBAbOYpxjvig;K;N91;O-SR4L%edOL5s2a}KOOhV^GR{*foA^@qLb7U-9GfoX39g*H7Mh z^Obg%FT3=dpImvtb=O>c`DN#R|Lm`yd(P?Tm@N_1Yiwu^=JhU~Jz{{pvLqI3OSiT0-{>WOhdO+ zQYD$hq6{@iV52dxOd%1Iz)@1Bp-<=C4jvgY$v1K&nKX}q8WfOZ5=E=baVrs&fZ_1=6UHC zPuW@aaqM3`wb=Kur9SrM6fPd@xJklB~9ee4Tf_W#T9^5YAgeVw~x(am#jyZGj7 z&z%3Wi{AInm9}~J8=S{(f7yQVLA$>5@V(zBwTax-xog;Y8Zz5!#_Z)~|0LyOUq|e} z|nAYJ7 z?qZ+=sKBz#<%9R#9UMOOq|e%1w#5t=`!lJIXQRtZzVvn8nevxi_-)BwN`3g?-M)DI z5#C?+Wo&OQ``FhFS6}qxBlj=3@3x2L-saDjA9?T|-^UindB!z6WW!}N}^?Ho7-yufP;7@P)D`k+9&B6e(qtMD@s9VlQn8J3hG!4F?%fgdF(tRmgeK>%%86Fzs`vH$$< z{_Rss?9$q&c=#93K6BNNFI(Z&%dflU8s9wa)PMRv{_%hN`@cKl@WbA^>M9$qJNXsM zdIGgI0NhfEC+#n52vUZQO&=-=!`#Yf8eo*?4d{=%eDcfgzg~u7s1dF-oG50PC0rHZ z845EcrEzkmWXy1j7?0cG&=5D3@UxM1wP-akZMBHO@JNX2I)!3Xr5m-;iApkMMg|+? zt5jW*DaN-}Ji^H|q7>ngv`8g9>XM*2NhxIva@|3`ge~Fnv?Z1m@t7S=2h34iwXPrO z$#29;v?YNnDB~8hq%91Jc*7_vuap8*>Z(P0D#D;HA?US56CTzUI?0q&#GEW5T#<@O zNx+FmTZpdZ6}jb^gv6E*2DU_)U&Fv`N9Tdjv|6Jg+f}5%M46Y54FhpuXA}UcZ)ihU zfhmYj9t2A)!&TR!L=%#EQpa%-TlBu-$;sM6SBn)AV~HAmAd@R7X?UBAML49Xt_tQPYe#k?qU_h*1;T^b!jT)7RRks14U2Y_wKu#tZMfcSl zTN22wVrflCAqAx|;K|Q2BNZfb5oT3}C=6~>%=l!WSA^htndUVOb-F}P_VSy-Lmv(a z9qTEH+gZdezmg@>Rg6s&SkHP5u{Ks!m(c~RGnma~$%^ecW$ETsCTk0WuY28i{pCLP z^~3Ye6kR*C`Q(!M_ug~!56(Tq=dyk2%ZIWteB-neeMx)aL-#zk#NWd?;s3}JPcD{u z@S%HezUBJMuejva+ir0C+|RwB@#xdfcniSWzxOYGbb;ggeiiu}|8@<@&78wGwZ{kZ z{dj^d`7OndEzai;r{ytYjCFaKdv!WXZIgSm4;c4@A=~u3VXV{nPU2_$u2O?!$gEH6 zRzyt@-nivnt~P8WqDU~{fn}~X3(N(m*>1RPARFQ&tE-m-o`jGl)pXh}b)9Zh-Jz1U z(kf6EZlaRbQ3z=`xnfhXsj528k|Izvio6s(_0Xj(btO>gU{jR&qe#0bsl=|dDuY=? zh*@+z`gt%fLbP&A=b(i~B4CrEGxJL{>@r?rBuDsJ(+ty75212G_$Fr$!0EPl7> zG2@q%W?%ww@=q!;Egl(cDP|Oj0Q9n3DTf$HEOO1F8d5|Wu^Np{#%;S>DbB1W zWK>VLOv@srh@McEDRa}BV0{>CmnEwO-?CuxHh(RuV6v@b0NM7j`D-6pf|1eNQ%0BM z!_j|y;aN!exyKgx|K%5+e&pv%=0EkwebSNz_dGK1jz#z1^4R=4fo1#4l8t7c`Fi}} zTjt+)^+o4><)*98n*62IotJF6$s6bFu)zm*-e~__H=d-O-(lkzG;RkOK$bapk4;g$ z&g{))8C%S;O~CbT8+&v4xR34(GJpJ__kQfa_Z)NRR&I|!YWE`!+-~+(>&{whB_K=s zA5imh%P+Uw^6>1tHrd2^U>mRYSpA8xcopQFz1?<`uQ6ZnePGj@jF3XO)oz-w){Yf) z40KcrMFO+~%dny7UHA%&3xEMnt0m|PUjwQjs^HeGM#Z1{@o*ckd z>kzJuB$<$MSDL^Gu&U}9#Ig2sEycEOskinTZ>KFeusJ!Hp&mPN;%c>^B;WKxjcIy1agC^Tmiq5$ zO+Z69wE}<9kXH7T@$i5~=_8{eV9p#BH4TGnf(OF&cJP5^`uOd_cfmGVZ#^*CcfG#y zh0oYi?thk{K~u?{WHIM z)sMcv@c!HHx%Ha6Z@Csy_Lou%?z=S0&#(8JDcVTQR=h-DsO=y7k#U_)xRplJnByc?29J4dfElh{gLRz1 z2c&eCF%SPj!A8Z2U-$wWicF@qm5^wiF?w^zl!vw5NG{bds2 z!W(#Y*PV7!q+(-0sUl&&m;{ahmi4UZ1K4_pS`|kTpj2lV)Y9EXEMvtLFdDZDU1%bf z9umwN5d{NB&ukkihL%>g(CKQRI)CGIZ<+a}acEgWv!MZ>JWi;n!0q+ZRo}8B;1+G@n z%Lst>$3FTIp!m6`Jing&-LE~TUcBfB=fC-lE5CE2S#Do=-j2ra9$mE1Lc|GMZn1?; ze3}~%JYY(`Va3UpuF0nc3dAI~XFHd&OIv=6;H997K}8LN!Th2j2a+S+3n*h)Ow`GFsO} zi8M(bgA8TrYZ>yHNilSFHA(@)fLL@gC8eZcKv6|0V$LB@6+OivUAa`JFq007D1%4S zgt%%#l1VZXq9|6#E{O-L08PG^OCD3uwl+9SIlbB9}#D+ zK+(SvqiR=OJSd7#YSEQBV~#UWa~|Sp)qKZ5ZMDd!8-Z@MOL@&r+%>b^8CdpetpqjO zowOgVNJL!{RFC~f@>xL+3FN!FZ3L3fQh8l(@JPl9&J@mYRb;5GcPL323^c)`o6yO( zZdeKwF~>rIn6I$aF8vYff<)oR>`Dm9X1DoE&ggn#HLVE0c56bTl-Don#+lgU*L@f` zuy&PV(@N6noirzN;)OvymiG0zcBAkRnZQ3PSzUBnXB_`0B;Z)6goS*VpN@x00o>8t67iHscCL-{d)tE^` z;W5Fa=I9^^GK(`aK_J>Km>^_LD-`-j)2)yrH5j66=)b3_IDa~(6>=Br3Tp+%inOa0 znb^?(9P!SegYv2bF;qdBXgGs{8N%d-6HSa9oz6c>5qV59NDKq*lFzr|dDt!ZR==R= zqk2|%*cg?#owp@^Yu=%Ff>`3LkbY4#U-+}(r!Akq{>c>={`k`KF8|T_;%aX=I_M1- z`zNV?_~l~2`Zv!@ik^Dt1C5HhD;LVNtPNR9BX*@ON;T$ zpHdLXm50f9&AkpdIqHl;ju}Ce{&l6HtE9y;ue@pzn?|Pz6cMQ9im+d|ub>LQ?ICMLo$UgdmTTUSJ2jAtR9d-g`}+HmE! zyUeycOaVMBY?&@`M&Cfo`n5(1lmFqmNqnxFu!@HHWDce}0tqa-%g zVt5&32681;tj2`E%M{RDU=|J~hgiuQ%9@7OQM3-oJ2DNEQqu1OGUT8$nH&|hi_@of z!7J}Y(i8l#tpNFw27m~jIU(VIFr%nVzPgY#W5EY_nI`CJ_0s`1zcXL}jpca&gIW-Z z)=^mPcLsZ%fF7LzcCGjg0kY86imzJkLkH{$;rgrPlRx(<=-AG(mzO(|-7#!m`a18s zrz?V>{h8E7=Y8Fg>?04`6J9>?groeE)U7|g%%3mA%RY{M*Udk<>*j0VWuM4)^_29O z^BgxFOpTs>u4a&Fg08gq!QAmw#Umq)h^rVNdQW84Wl$8AnueNaRw|O$MDw&-WDb6r z1Zvu2M9ed--jJ+{u#ij?dXi>4*pD8NtlxZW@3yH5@zrY%2>^PcT;ocJ4iTd_4pl)U zLOQ7T{tR8{n;E4a@)x%RJVWgFLU)d2L`p#?_~ zsF(5>(wSslrHmLuG+wkOZ-+E;0BQH+=wE- z40r(nv;;u7h>|>v-gy#}1(8d&(l&=*4Ct)XL~za!76=pwq1np>!?IJ{qO5)y@Fg@a zFLT|yHr~kQ^1OTJE_&!eTgm_McYiwL8>er);RZ(^d4$`?9$xgv-~D#(U3aeX`js|$ z$A+hW?W^J_%0P~OG$6T^#II*Mv5uyjB=m(!%yCPB-B`6GUg89~N<2o+o7Igd^<%&o$ew?ZZ=Q38FuT<0>u83L(*wJz7T-6%`JcCGV45Xpc!T8Lp zs%w!NDw+sxHR`I0gkKQ`iA+^kfen%5@N%U%6BS)^Mw+H6uf;}Dv&mKK$`G!Ie08am)r}MC;ioX7M4A?xl89VKEQQfY8xjan9ffciTA{dN zK$P+l$ry%zaALzF5$;xtJem!&mG~0h@oZyGq5aR zhMLQ}mBPzHXmd*I^dZNU4n#>BLYL&pH-L4P5u#Jlgfc1FFlcb9MFIxO(0{C`O5G~L z49g5Q3Nu+NN+kku)``|1)m?c2rHp){$ib{4ij3QeXj`wTQc5wL8dcgbqe*p5E3M#k zRhpUfPjiNgRH-W!?Th;41IWTxQGJx6JJFzwQglg16j!vSD0+~g09akyZLUgP3yEd$ z=n_1tXs(88i#b_DTY55tUCc-10Y2n;MQ(X)_)VsYGT0OqA98~6sTO-tfN0GX#1@^p z7LLkfOOdTSwn+f|^1DJZHPMw0+9H#Osizg~TA344>`JCoSX;E58}UdPUUR}@Y@=4U z>dIhXvme=Wkgx5{9@@jOROGIloJ?!&i79Dmr|Epc(%Wea&FT{y!>0M1S$XNkG8S& z_M*9W-*o*oKf3*vYaV_0{&^41efpUtfBUDGe*3#$-F(aS-}vU&&OPsYbML)Ve&M2d zww0x4o_*ZD^JPE2=m-jBsAjf$8L8JwF<}ccPU;SY}>p4fH(}Cr` zjF(5Jb2594!1meMCIj-YJr5X@c#POaJMYymJSh!qf{v5lpl`mN2HogNRaeTryd9*n zqwPqxs}`MwGT<9~m>VY)lV4_vlNHm7A=1t8pcuM#F%Y6fx8$IMz>>j(IVTLFM)4p@ zcxsdnGeOa`aII9UOrUIo zW3z)~Jhhcw<%b*$IGJf&a#vm&{FF}t6h(Mi<%k-#53xic@@*7(tz<|)j-`yN=IVl4 z$2qi;aa-@yv|FjVN@Zejje#`shndq}LbM`z0JSWzQnCn0lb;G-{+EsEb;F@+e;p-( z$#Ah-2pT$;+E$JZK|lMHm|=nMec4}@9L)ZMe`I!Adj>E6?iWjb`@H{R_TN(TAmvBr z-?sRnTV0=7a_7^J-SOn&+wQvMB4GLcJAZWBwP$U!`CIm!y&=53@6NOQZ!#3@ptWsf z`^aJE_LsqDp91r*Qm%kA)Lc^f;eEDn7~5YkAFm%`3_n6`Z&`I!6i62QM2Dk0 z8an<~#yMxu0B0X11}JP}0nwTO2F2&Vscl?&P0J7kFxv1E!`sNirs+0(fmX3D3bsNR+vGjMPp;n+~@SL6ih<86dP}suBP|2HDd4uhKTcV&k$sLU6AD#H& z4}Rd*>#wk(eA>w;xH@`muh|)FSHRh}vaf!f^{ub^gJzJ~&a#)6&;9O~k2-v>FMak% zc=jf6dA3UGnv$B(V$ETbAq3AzNkA{UAY5J=p$JZL z)MABb89z9TfmgXs$2COlpZfl@O=rgmaOykf2ejxLZa$puCzm<9?WUV79i;hH#E%|8 zt`BSbvB)6N3Yh{A41QT6t#K~i5HknOgw4+I1F8n3F~CM~i0F5(J$7|0oHCLl;+!Cy zusSgT1D%C@gMQ(TqH70V-kk|vkWvBfFRmxx7eQY;mSPRKnlXp zoZ9TtYxr?y3r>+6;pDg?;&(;oWJQ{Yf!n!@1QQ=xN6c{26zMqpdWU31qY7^rd%6-^ zEn*;tA+CrTRf|`ZqGS$!wUmNpAukMWOWKv8r-~S?72$Vun&^lcF`UawVi~zWn2s@+uOW zlieypPgGq@;B>;A8cL@V=aevOHz6o`L@J{#nFfryc<9;SA(Met7>NX>GOl@5#nwjZ zg;HHbdO(D$d{N@S%RwpuWHF>@d~g<`i`lBBhy+OnC1|WvK&WmMRYoAHNK7c;6sclx zn{!yfH0ER@b109<6}&1jwjRMBGm4~^TSQglRgZIcgDn~oSW-z6g;D^80u(ERPM=iL z%1Cq=iDa$tD#@DADT;Vi-Ha-@b(J)sEg40X)+l8Pqzi@-rK(VAS`?zlS)z>Qw9ZVe zYtgN4H5A$FS6MNVU-HF8^0ewxS}05n>`+x&=!F znkG6WC&=iC^S}S?bI<<&`MMM6J*(>8z3pmmd)FO$*Shz9-v1ZQdT&DR zyUsf6Jp1gk_dffa=Xv(;Ip=v^+wH6xd>vjEQTM{5zn-zgTz=_A-=2H%ITo7z3-b+2 z7emAke(xUpuD^Wd>3{g<*PVFM`>vjU`I76eS-0N)klWEW+`YD?=S$~a5MK5wJ-pn5 zGDz8?vyCOoZ(geQ7H`matYjy)x5zy`*v$HJgKJEARpNObfOFwxKCxNa!vs5}Jx|~$ z-r)Z+1Vg4_;x2~@l}%}G>`iLICj&I|K&i1K!MLM4Ux@X+3-2#W28B8e$9&U}*kN8q zPUqCcGbV{c#B7se)|^JU-VHm!D+#BDcxY6`s)&RPIe5?=Vyo|(44|jnR8gR=RP!O3 z-{PsZCQ%4iV4FNe3iwSg1HoX-v}b_wbQ}>rgF<6tkR)A!oV@aR^i{5sQc| z;c|MDFpZZ{BvmPLL6CyXE={7Tl52%4hV!~h2L@~yJlFE7vIHjEbnOsihLlS!I=8s& zT(*0}`f?xpf|vc7RQs`g>C58s3~;{LUrJeA_7}`@pUA#*&BEJny%J-*v%i#rmu<tLii1XK-1tM9viSlxv+!WnipvTPDCL$n2c!k&FjMissa@uM54e?;8bj^0H=$V zamo={0#n1h){-c~0If?}f%cjJp@0bnu+*V0Xu?Pme0>Ynrb`t-3P)RUw!FwxD}aPx zmY^p>NYFvR0Td!oNNm-Q91RK7gh0z)J~&JcMU3&DrhWmi$^`min2tj(puMhX!ch#i z_^z#g%%K9C<#mU4Yl8;-84qLVvV3eo+5i4{^1y6WHt_-uHh6nCmd99Sm^25#;pIJd zefG&GzT2n0fMQ_TYI1KY`^*=-3^;?y)|V|VPhMm`>(ie;`CX@c{5|$#fAqvZJNcxy zf97NF2bLw@$G+x@3w$Db{$=O;KPl_W@Uk16T^>(A;651|?nSF=M0JZIs4ttev6qgkzvi!cPvOq!3Z9vMT(- zX1SQesqNvpd1Wewm$}hn5R1J0Zxz^(89sXLls0)JPk2P^M6uAeYHO*sbDP74z>^}z zTXmIdf`Z>OB~N>V%Ej{x(8_zzb|zpjqhM9n7II7j{OF#L7-(z0>{P`Mg%k9O@Fj4D z2PDCavxz$RMF$BF*71@_YpvV{3D2WEXmV<3^Jem2ibmT$jT$llk0zZO=n5F5%)_H$vl>x>>+ox3P)L&_DiCFThdO7y5!XCX zW`f!p62%o%XAE@`SCGu$&IOeCH6g(m8AlzYNTNj_3282Q;GjTP2Isey+6mz$v%<`w zf*IK8Wq*d%LDCR0laQ7e@YBmG_9*9J^NqUr$xN&qC)Lx5ove*>^qr7;+U13CDf93U z;uO$|VGb$M5s6qT+JFrv1IWt$!`C19dY{oDfZS`0&@?EGg6?!9oI0ZPj)c6;$%s6l zQ>u&w75wQY;yS==L{-|dM>z{g(03EnwA87Yc;h}qJQT_G$Z1PO-lI;x8c+p741|Qa zzLz`9Dhw&AiQKrOkR!*#W>yhJY37ljISTD+xlR*AZ4+K|WqRd=sA6O@N`NPrZ+%U{ z3XwEWvsxG`@audRa5}3cWvQ4`P$!lnK`Q|;ViCKx0p~(KVdRj5t;ktSC}Akx6DVf3 zJhSUKvvo~bubcl3oP4_PcsPM8@+baUk*2c-OJX)lvYFX5(q2f6D6vFRcqm?-xepSU zGs_9&Ofscm0M&e}U!)}=fpRK@^g$0wgD~V{Q%1=N!w(9D!PAj29@b7&Jbag=ojvsO zgWN22sdOoBWkKlWvJg7+BCYGW6g)95{9u4sOD4$<8+0?@#3qmy$*JlhX0{XM98t@q zjgHgP)o;n-E5P9k&+}g=UlmtdO|}l@6=v8OI!5spv#r`{moM|hum!8vc!6T&-S@16 zh|l`!m;U^nZ~e?EAGhdi_qAo^`ybc0Gz3x_9d4Q$rsfiv!z4DutT4*y#iJFI#stHl z?{bul@UkXNFs%sG1;*g?^|O#5ingk-1qL$)k2G{m6SFf4bqboNodF8NGdM9!?;B_N zbjm+jC{L!GoN2tAmts&W@%c!N%joHFjsjx6)|O#7wUR5Vo%U5?u}MkL7F&na;ukyB zWmSl3<*`+cv#KFV6TX!cbdCI)IK*Q_3_-rCd8+X!L~&tv3y>5fS3Fk<7b_90LMOFC z+fjh%&Ee&im7T?H7g2znVQTBk;bp+tX6!p}Tju3u|G#|4itFUSvh`)(`;z?mvOkux z8GFUDOIF={`O?MTy?M#`bI&{V>@R(2*IAD}dd@D#9P&JyvEk)bjR9d8`Ax^{VY_t* z8bUf%@(W(ZiN~wW@N(&x19mv!u;={IaeLUF?UP@J@3-@Av!C?Lr)+jVwiQzG zDrgDzLV1z`)abh*py*`nh8_u0qw78=!r z5c^j_OY}Jh9zY9#87dbWlZ=gCoXWPpVzcD;(iiW62Q%zxRfrjYDxPd(0QoScu$eR~ zQ?BjGL|MbNYD|LVV9i~k?We^96ZP>T0Kp;=ftq{Y6O_fUQI3F`F^Uo&ccxLm!4w1ZO$)O1Qp2&H9Lw7B&Op zU;-EgOtnRvG^hdAwxO3IP54}MKn7lxUj2$Wpa0~C`?QzUWUI{WzHV2xC1w=M&Gu~j z|K(48^qnB{>yMiQET8i6_gP>5$`?LiarxY{zW^`KzwEo$UVW)AV}r~aZl7$PES4xP z5RCv;S{nD>YcI^EoUeFUq(|iV0J6T*t};^&=TF{P6xfA%nu$8(8$A-GF#U?uF=Zd+`cft457Od zhy9l0`OuOy*D+NS_UJG7Qfi-u8T*zuzRq3D)m{|gPUP{s|8n*p&AaS8%ez;i0{=

K3EF&KKvJ$4wTmJ=Sn8c!FdF+#s51l1?#$AB-kT;AI_T6*@O^g<4%O zQXy7E17Sc_@d+cHk8%i<>*C>3J#xXX~bCh zkfGo{=Ik{_kU6l7B9OKSWsNDkT&hh`yX?7VhcN|lZEkeE2N|NJQ*J@vCETU@pX zCA5Z`dkfM&Px1EUm!5m&<>#-u^#<=XTRw)D*RERy5npoY`CmHY)bq~&Htc-<1?Swl z@}_(5^AFwYtTIE;7hiJT1s8tj>iP3pLGJAK;V*l$Ehr;OZOnGQ1IsYAy(L_iyYy?! zE0hLT#|@CqN&zBM(fs$k%>F0O7J{)&PR$LICPStX(W)U4lB*VCr{V`|!wEXF^R&@j$nVM*5#eoWJG|E*; z=^D087gE%?l*d1O$ifDid5B3?7%4^KI>{wMQgB&tnR0=$!h+JB#kC83?n<(`{n+RL zvb`eKlqDy1Z#o0ZHe>rf_6%@ddH0&-zK?A)wk2kG`L5OS>%+^w`gPCRrS@Zk%+{Be zUw`51<#QKZ@wM~5e)4Y5e%!H#?)s|3Uf^S2_G3fGlAYK#TH9r9`FL!-2ARXiV6p

9h>eAPvgFMX}M?M5#z-?x7986TmMdw2Ic1yO{19J-LhiG$)W z2Dvk6B!Ne_LUhc^DAg(>66FLe_8=OCdo^`5rs)wucf)HjV(G+1}TrRr%hvp`qI9v$kH&^$^LE9}Nw( zSyV`Gv$tm*&?w2qQNRr4i2;Po0tQpaq#{jF4N)3;NZig1$ik(D1l=J@bRn6&63??X z=)qTdfx`3RD9jFtg0N8Z5sJ4&4q!4HRyE(af)KdC05Kke=PmY5cn)AWTl}LffwYTN zF#9YXM6a`yie$7U8NDY$W}{BIHmY?3>XBzwy5mKAvl=J&QUr#38eqZQ_HwUaIEjej zH~d5iz%*%98v6EpfIwAx#{-On8k`-@5l-_Y!Rf|YqI_2kCkc|iGg#jx<4dXL=7xz> zt+8f&8VkdW0{K#{$Tt(5zXNMyHu@xx7TT3-MUH}2=!8?mW64wnH5c+35e%j!IYL1$ z3XU~J@nFyvk1~N!l7d>qsuY-KthH5eNE(wUtx4)=Z|LReYQm7IU=*@bTMZ?LIEUq$ zFg|Ok$N)s%U@&i(v^}3;12#$D33t_X#WN^4lR%fAvPU9;kvQd=(4Nzh5T{yfm=UR# z$RR<$^qTQ%>sr><7EcD4s}k+?(YTxcI5nw*4iSm1%WR;FeEiC!p-T=4;bclB02wrH zyS3apa%Jx_TVHNJcJ$t2?!uCGVktFg%{?a|Q#p+adYxP#xzm|Txq^14Ho?rXhBYBP zHf9Sp1{(rC!Jm9=Bu}U46lOxB5Iet9Zlbv#hl7>=trWa9xM#4Ew8FOq^5lg9l zAts0A&cOmFgQbaDRSf|lIxYqaFN3imxFE2=9;Of>hol^e`GsmgR&WDFLszu;!x5y3 z9t$8*;!Ft6U^2S0oI0GEj8I(c$1OREwRy>*4l-Luit%*HRoyjNcetqjVyj_()3#|w z4nM>>kuw7gbVU3qjc5}^2Bm3$p0#GR^B2rpeC+~{$U4V84g2X|{piO(`9AF2#*%iMIQLQP*8cyjFY9s; z!+e``bJeYKu7Go!#_d;!e7T}&*<@}U9|9u}@36(dfu@R?tl`*HjJa0vJESyea{PdK zo3TBPZkol9&ERUq&PPm5l8p>QG*9G`<`Ru~fG-6JeOQhtsYSu5A*HpIqYoE3)Yc*X zVG<%r($H%-v{a>*SUG74#7T=<8ez#$YLp}><4Sh@&th zpd=w9qldfEb%BKXTU&~vIK&)}yxNV#qK{M`q`2$+_IsQW-_y*M-z!D0tKFeRhV8s|Qct#L zuV3YRUlP;|EU#a=)ITr(=)T+K{!eP%%IkdZ3tnEoYVy^uJ8r$^?zM~k^YVr}7QxHw zR$ggK_LUc&_M+!J;WbCTP&p^O zd$8Yl)UK~PVwd9&pZT8loaY|6=k_~q|Cm2`>}K&Y;07VeA!6tZgoKMBQvkurGK34d zVhB*TBnm2GzzG7lz{0H%t~l8u9VF;VD*z@9kAU3YH(P;q{UjW-1Tc~VFDEmQ4uo#? z_{W1V)_;ZNU^O}<4rptL0%AK1HOtk)mBL_hn8!|+_rBv~u}`9i4cJ&^fw(1RumU)O zKz+{0O@Y}CH7}(12B<|YVUM+7IEbA-`5Rug1PKVUgj-@3(hH3!ZQ+=F8@$17rs^;7 zCD$W4+eDr%H(Os;)+tDY@j9h141nV>gJaf_#j)Opg~A|l#$eV9PPPILmuG-4l^Ku{cvW{#&_Ickt(|&AV+4gL?WZl`G?Js`rq%%+d$XCDgvDX~)viH3G^`HCX z2mF~7r2O4)e$l@$Uv=qstS>KFaGCd)?Z@`7Qi4@ckb9YP#I3EE({0WOy1zSF)C)gX7uI;EN07xZv{O4sguta7)?;xFzK(6Vx$6oF5(bpkS9?#dvhIRjiDLBJla{# z0J#oo#U~h8OPY>wh~Y3&C~760#jijMq^y<-1?QC0Hb_Fgj)&|NG*LwbP=lW+ZTUn_ zX;qlDMWL5E-K2tc(I#A`iH0a_mdGKE6R1X1l|Zd<5{1sadPiEKDB@B{!AwBcQyw;3 zv?atGXp*p&%~~1CaSGr;p-7&tDAW;^ZIrT>UIx43(M6zc>KB8lAQ=%`u1Fv`YH?y< zu(nNCbTdlx!KN0QoIrhKBp%*ys4baP3!#-YVU_0WuM0S7r`j+{I7u)}K@6PuN74jc z*DxSDtk(|f2s1P%lWGP}dSpL<4~sEo=3`$rW-EZkC1q7Ma5}VmbFxeGVQ9m3#L|Ug z8q0OC#~?bwRChgBiJYmkd82;SFtbTu5Azr%>8vMCeA0Y2r6syBH}3K}Xp}2w37H$Q zsby8kgeeJoaxc09)eXlm2@xly1xT?WroD+rk?!crFosEpyejk(^Fs|lt3XlY2?&VF z#P%p1DofD|+XQk#N@z<`5V>ih3V-0YwyGpVzyv(O92-bNnkr4?%6MoO^|4T243O?H zUUwWiPnpW44(k^RItn(0d{yocvB7GBkruzUv{0H}xkG&^<`m3}LRrpQbfzlvTO-9| zWD=NfqYq+MQLU|R>Q2H5DUF3EhtyF@MQj<9H`HbiOR(X|fz>*sh#6fm>aVg{VFsGI z=CFv!v$CG~FjCyk^LC+!OWV}Ii055D(Ze{$jn-T^O*uDt~*%$B#r>>FRUVN1UH84?_Jj1A0TI)##>m%v!OEgfwDixfG{EJVsp7 zL3%YuDQTr`%s3^rl7gyXyz(^0q=*MHL$2J3gQuQiK&K*s3Ra(4%Yi8P*;cJWUdkbi z#F-OFP%GiDN9@VN7|2ODhRWsjt9&?(gc9ZqlQWW1NtL#B5Z&RBNw1Mj9htc?%Oh&V zVE!3;iE>B|QwgVqG|W=kYluw&8{+2Z&R-g1AfH&Rn4}=7N~uDW$t7;)n%RX>*(J*r zL$GHsJE>g^9jbEqa*{sc|49MM0J3G}0hvAcn(P~y`QDfBWBW^~HOsHP^@as&ZeDDO z8D6%;?5~z3c=@3}U%q+1mzPV6t~&d+8!v&E=biugU3PfX2}kX5{3~Acn!{gUOSbLS z<3FVO8!4dJBC-`^2pV3tvJ6D`?JtLjUUOFar^oN}N3VL3_2pv@+7bN~2R!?r7e8~C zXFYzKt$x3Kyqo@?6aWZHfWS5(!MjfWjxe?8DxgA zeEW+fa&iD$Yhl#s5lE6``@Nyv-j${_WZiqCto6n>*aB7QzFy=sPS9meUv7ihT1J3qcTW*GzeJk7FOZCaGFMjr8 zl7C+Q+8G~z*V~TweeBPC?7e54`jO9n>O||ym!AK%i_ZPZ)pO5Xvf$Diu9;<`nC+s#dw?MKa3n zq1`=|r6LMq3o3|0kvn;vp#?2!Swf%!`9$HNPIH{9&?SM(*&3$a6SpfH zF0;n~Ruvd{Vc%5L+|+7)69YL(6G#w6i{@GxdZO|P9?Q*UlsV)1hQ}IUV~BwL_&v9g z!>ZS{_}=h z-w7AY|I0u9J%n!7nVC8sW+Wd2IW&p`J?#Oy<5#6Xtlbnx9WnVXnI}}?Wv8AhHt3F1 z$vmXmvOA6F_Ued{YzXfxJN@&Y{Qd(5&b9Qeg;&qr>hZs~{kBiub(dKu|K$hlBfD#z z*z8)hKmG9ot5@C#5IH*pPUJXuoCT7z55*EQqV6-8&%<9i0nuDC_qYh%s=Y^T048ho zqT%MHfhbA9i4sy{t-c!((wgl=N!&t+TJot@g+Z?+?h;QjL#weyql`@x*8!I!3=T1C z2%QmVZc}QYRT);(Ci~eeVN0n3wx&7>!b^t{l?gF9cn`;MUvAvVnnLNX`(ohn|RNmsz*$sJ9NTZ z&QmT%)m9mHBcBj#MUR}8wd_>TI|V&rvo!6xC`~_}afq{|j8fvUIIBwIbOg)(5whkD z_DFbG*yJQ3W7v~plPM<_MXehgbJ_R4Y`6Z*C;#HAc^8PN3$uMFJJc-XwiEmMYp;~-$F{!g z%h>)jYW12G@UqW*`PdiK?Bm$?-0Nd~>nuIX?b%jkiFw(LOPtGKvL$A?7;3g3yU%6Y zjE&-g04%!*xFA@7)(egb@5mkULW6EfJCyVnQI~sCWM*PemK4r>2#Y~HCVn`0QFgOMcnWTV6QYGbH%u#gBG$X-b+R2n+))qe@ zL?z83qUPDDxmr#(EeXjI49rHC6f`MECzh7FlQBd|suHV=pK}y6mut)Uw5LVy&^!DN1pa!K!?K#!_Fa1fjXj073Sg0!wL=&)uQMHn-Ou6$p!@p zkEDPN9fOO*Bc^T&u2)?`6tuH%6J?3)S`eP~Iw6{OR*RUmYL>CXs%JDd<`{(6e zVZQf{TYT@!vhs#CH?Fwh>Qy&gV{zGEO2Nw(ms@4FvJ5Y;zGdFxtG{{K`KK+t=A3iC z`jJETo8|wPy~1pL*+;GWz!wzkm1G~j?w_RWyGD@|plKVntuIRs@x0|#d&z<8_x;g9 z#~(WTl?OiyfXw^bUk1k@I|;x9-$3T@va;n@2?dS-C_q<`Sq|-@ zV*vV8L4OW`RzbDU7itJZgLu%WkQ-5spAZEC$$<)q@;pIJ6D$tu)%mii< z6we^n#~2U-DIC`7y6JQbQ0MQl?Sv z&!K!q7!*y#Go0{w` zx3cD6s(Ax)?*(M z+9(b+bYFGGY2r>wX1PLThZ02+er&}RT4{??ZFE%`lWJNX&m`No*$80+>w*$GBP`N#L&Ib61mN&j`^2s&NNrd7;dXix}V`suxZp4sR$BTY{GX8IdY2 zrWtD`W)M!PhJ+BV%oTP)Fxbkk8BSM#NVtwczg!soF`Q>O|xH!)cl=qoptw}R%4g_;K2>IuUdZ1{L27p4V~J$qtLS~LQwwefBC2X^*{c- z=(o4O4UmrOAf<+$_x}A~|FhwHRZ$2;(W7t^^T07xJfKR$CJCh`gG9C5A-Xh;qtK|Gf=(nyxG z7LQtOGlPo~*!07ucaj1I{H)S1CtXfZ6{r)^JeAVA`3X*4Sk^N@mM3;FOqzhkSP;OU%lqlO|3C6PSb|69vhMX{u*~ zoZgbMCivNdZFqoKs#R&FRsrYeg+rVuR3urR_Ns8EJvKtf=Rkf&cUa%C6?X~D#PA~g zg168_0lz8*Nid1Iyns-&S(xXr$ZgY{Ks;bY@-UEqayKn= z#IcCt|Dt8xuh(kP5p$XX8&KFFl9yK&Ejuh)#;GsE5;hoL78+Kw(KSjO<=X2aiO5b2 zI>OJ>crajs;@cFy(yTxQb#UM0J=~f9kJG%#W=@*(grhrC9Fpl2keunxO{-2`VfIlk zU;VP3c!tdvH|5o9%$a0X{Cn1?Od`i z8p<1(m#;K8SWN(VqmWoR3dx~4(li9PBn537to(`1sNtbSEij%IIe?ZxZXXGyCJF{x zLuRXuvq>^NqiU#0DYlU}-xx1NR3HSiLv0<_$9Paw)h~34u(4lXFzA|u$6*rG;z7(I z4tZ6-218=y!%jR7hf;?`)kj3lH*z3mwWH&SLcu)5CJ-@tRS}gD$>e`?9#P^|0vS29 zSDQmz9AeuvpH>=k^5s=cH>t~VT2iE^lu9uVqJoBq!;^e2;g3j&R>mcp(}JjL%Ggxl zZ{Q|+H-neE(YSiJQb`J3$DE-h=d5pkNeU>wJm6f0ql41UWd)~so3Y!Eec##@_xS&~ z+aH3=mYDBbx7;VcAZ7o&?9ZemVA)?Vuf6SR`SR<|yZx5Szj?-qFPy!lmzQ6C_-@A? zwyRf|+jI>SD_Av#f~_WRgqPKNc^O{DW&CSO*jzj}H^vUn}!e{^Dw9ov7&Dg;5Wfy+K;_|Y^^L+2?)@6(CS$nIun8&?L#8rsp-X~IW z=&oAyI~|xgYf2*ksDKA^fwx;H9=B$Ed@#MsPUs7wsvq?)Dv0l+NVRV_VsM8li4=-D=_N^^Ty=-P4!?cA6rM_4aiDL23zAxSFInSQp*;zY0)xNuxw=DV9&wuE_qwD7{e)fH%2Cs24EqK~z zV)5E(V(W$1`CV9a%^>ms=g2u3if1QG?PRg&uN7P2ZNj_J(3h6l0Y(w9yvA8S<49= z0`BR%S`?P2f@H*8M1qDW42XzLm9msKl8-L2pIC`Nt+JuByS{6siUd{Ev0N=J^_^8r z;+F6`fwbp044TMug;unyMbTVB$1sfNLvzM0hQRvbb6&Pv`_zxEm||w|S@0}{mwSh? za5@mpw#=hTf$Q{?x4XPhts{h+06OJlL6I_&J(^%Pf=tSIW*9z60Y#PBWs+!%vwq3- zYm~VNn`;yX+R4mU7+8hq>XZi>edVe!YdBhISQkbwRAG>cg5=6%l2a9aLfA%NLx};b zV~m1a^e8YBMnXYOQDK`hY%BCdDTqZW&J>|xC^Rh=jZVVm68U%pqC7~NvI5EYNl1H9 zCE*TP#o#)qJ1msz3yP#L(x7O9M;}X?NQA_xHU>Nr`$rds4Ja+86~3}WEmwK0|BHrn$7z&JOEa+9x7E!{SCCVdOt~+&&2UuccO;-ju zZQTU~PF>R3$q%73kkEh{R-Ts>e(C-(V<)|nZq6)kS=wu2>vf%?qM74Nbefpe6zO;_ zb!uCUntb(orH|NO>AOI!O!W%0s9TurBVb-$_8IM^OKk613=Ugd_62NU*;2B@8*f^2 z)AH+m{tMBTYz14jRfU&58FM=KS?pf2aLRi`Cf(p7bJO(-C;)Y9(D|9i8i<^)+*liE zGjJq_@N&DO1G6ToalxO2F`DLT(-nogXi?xaUiH+#4Tc;Nv^8X%8Z_cj#>VH#Z#dv< zwNm4Yjk-ozj@aB#P}MZZW^}FURAw}pan_t+O(Z$W;B5YAi#TKs8!|&xO`>C~n-1yK z5z^vOB}JDflZED0WdhMPkrbvo;xJ0Kk*px;h@^Q+^8iY!M+c{6rUC=D#BQ3qrm@O3 zXQ(qA_~n@)S4A($U6(c$f|5NoVN4x#n1vZxZQaR5#7dmsVCkBMSyB}?he=bw=0YSX zI3!RLoYPIB*JW&$o}IRayAkR7uxaje?wsxN=qz@KXcGur3?ti^EvR!S@N?RV@enO2 zJAa+jVP~g#`?0MuZ&-aZy!^oWl{RBrVt(+hmEYTN+xIrydhgns;N?}zufBEJ6_W3L z`8c-RC%?SEeBZjozK=co8C&@OWlPM5@As@DU$zr;3?o}smaH0EWp+3yfG`Hhj%$#) z{n#KgW~IOcG-Lk2iRRAxYs8ER%N8%iG3!?SX6rYdlr7Lg1Cw0hm~$8ERGHeP3jbKMkR z{CY&4+eDb%3bkvvwwllddRu5?9=IXlx6r0*Pk!PP$)Si4eL;EIzWWmHb7$1$A|M*9 z5XmEI1@nRf_N;;)4AXbsV3jV@;xB>h+=e)SFV7;lLR)2Pc0dYdOYl&Sf5q}*vK`kj zGQ8}aWq8?AvcHjP#Tk?iEMq{o(ER1o{?aG1Pkis2-~Pw1vc7!!DJPwN%7-sF=gT%@ z&%5}WH(qvF}~C(h{?%j|bd9yU`1r-0wtyMQ9@qbf1mlhUoUUX%e%E zuZpEHV{`x2B+1HB(Vg1SJT??bt<+hGVWTP#&pDeMwF-=M$7ijw6B#if$(I`oo0Efe zoC#<7;InX7L(F<@pr7=F#{s`kvx(T;QlWQZ8%;dr04;sjOB3@- z#3f*0wuYXl7(ulLU2IIwh=CpEX9i2$LK>UV1r=aIKm%Q0G^Cy13b{=%)0-A3-b}Mg z-+zl!WL}9$U=6i6<-Yz63p0S(ChBkh*0)rdFtQcZm6%f@Wlz0Tlc1-s1No;t6j*_k8Y`TM{7 z**o4gdBxl6{BF;mb^NQ35EuRJuYQ8=61w23i=BF`m3&*<4wG%4Hu?KG$+II59K0~@ z`IT(|JizA$k#pF?Gd%39_Ye%{;~w+q7d&saM_ZnE=}0ryu<92i;!uJ8v}l{Ugmhm} zWjMm8;1#6CjVR3}oDdDVFcJoI$PD8mm-vJVN{x#_VHhG6+A140sMbRUNv*>{cSvCB z@R$Y+R*T2Ha>(Z-BkFjgPar?wQN_Y03F8qZCMn}dh=$V8$OGsZh7+6S%;rxyP7>m0 zKLe;9R%yi^4q=J@svRkR(xSOsEs8{V&2eh3A={*EKy3bNg2JP8_dIID~Zy{XV} zB*se`GPSlfPdEv*Yo(xfuAd&rkVfM~6z|P^7Q1cOLS@P75_AkKZMjDbyXY~g z{zi8?I}I_QlOR`?l)I`hAc|HajerM#4N;1Ph@~_M8BdO?=!uo<+9XZwR1_`>BgK}j zg~1L*gYprs7w?JLB;|2fdDFc3w!Fxed>3n!!b@RZ0U-)43@+8FdZcT4pw5;@8FW38 zR1FD3PNLM3LuqN+Bzgy4u2x4FCLATC=ELDM8ii>^F)(!k*r(n*)?d<+gI=n$=y^kJ zu87V#!h4>NZaAdmXo8a&)Q!Hg#N0FMP~^@LGt(3`U7Nk(1pv*P%PwSnA6xIl;YJXS zUgR0&Mdy3Lxy8XwJ#BToE-RQPE_cpBQ`$MAr`g~X@c7iq@+B93YyOoNDSKO4jO~p` zF}CmMk3&dOj`SKF*i-=!p zlIMA%q$Wnv$cKNu{;Wx@3>G4vcLpg0WHju|LeYdS1C7EGCJmX>a|3;tooac^(J`A* z^^pXJ#+15J^AH_#oNU$+<>`K*;28#t{*HzUcuMKjJPC;Gk$1?v<}J3WGCLmm=*1`? z@JeJoywysViaYG!y5v62|*z$BG3xJhELHo*9wXxLAa}lggA65;|EDZGGcP! zw^tXzPi^TXjaVu$kkEQG6sZoU2<62av5bQ|&WKs{Bpxnp>`ig+KA( z_rb!jvn6IPFZ<+|1!aH73`EN&x)6`<*Dxea$Ve0I3kp{j|ewqwcdEimc*vPejC4m{ELKOoc&GLNstPTz)g)=?4)G^Jtuo6sN68WliIoe&4JCN%wPh&GYrjAl zuZqdtwZ#bv(6n6v7MJBl3U2zhfB7AOLSmhQ%fM1o#qFLV6yt7C@F&e7Oe)@B_CNmi zZ@eksp$E7MIC~DG7ZeFa1Pt5@X7a}9a^W!^c5XBTc?qW~!XYp5w(!FPepB6Yxy{)6 zqRc1Sir#e`ocztNf9_3Wonkp?Z0b@?g=JZ)Y4k_x{wK?VwLUr&bvp-JTn5AWs_)|g z1!T(Yi~il;|GFnXz$vAkIe^LLxsPUhE@b2P&O1HrNn364M{j=h-~93i-+yrJ&whH} zB^RCj=-=A{@yNsXJN(dnUVHozm(M+C#m$T6UvMwy2fB@%6w(QI}Q7uwighb zCLsJj{N`7NzyM)mNl?ouILDnP9#-gM>+D7rL(s#Z8weg#;2CZVfat8@Bg&lN4LLf$ zQ6ZuOd?;!Z(6JGs3by3|(m09)^yaWy!-~QBGS%j7bZxPz)j?H8jo8}iuUab5i5)#+ z6@Cd@Wt`1qT_Bl21?t$z5_3rzfAmb!oeXeTN`joU+VG#)xegI44kv2L)YQ1WQh?JA;Zf8W5Kh$0=ztsa>Q|HhEPEr`I$-`3S)TB!zE9O z$ft-8F=Qv^EL6aW$R4%&%cb}`$2C#N5BN=KC#JdTn7j~KHIJB?PhF%vyIvQG3rM@e$Wc?Ei0B;V)ojye_md&@Cu*yLbMYb z5m>gkY@aqFybKZBbnT1=iX~6XY|@62Z8GsM%WuCgkBYjDk_{Tn#0R5a%hfB#GZTbKOSWXz428m zHP9G_aPlQ$8#U!|YA8Hns%?ESyG9xfNmJJSBscv~9BMmM=btD;Cbqh-FcQv~laP^d zGJD9;aN1XubKR(N$Sn?8MUF#qNMJyEvuBv7h)a7ydk`?7=|H;c@Za4lq!Mk<$_5L!vZ1*6T?1`G>(7H3cEdAvEJHyL& z-geXd>u&wN$71VOcz@Z)zGhxzUh4m(z+_Ry|q1irdOUcSXW*^4}ncw=_ zmpFu%Uva>5=j^-ltfxO_b9fo(0}=r@z_u;fkR2#zEmg1znL4Z%1cppiX`)cWKrv3? zI$&5^IdBb%YG`c~S_CRB*8-t9RmBfi3KwVI9o_BXwtw83V&ec1Lz! zF64IeC5@0Q#FNm!y?9{O z*R#C{00^5%P`oq3toCrm2iSE-P#p%>&?2wpb!M|!6a505%|`xr3NS;#BtQDdBe~JK zwnq?LdeR5qr_2B6fB#RCx7u=xt^eTBa}Ij(kA86HUF&bT_wL(%^^5PH_`wf6_R(8B zf0ym|-*>mY_I%Fv+imrjM{RNNfqPzc#d+N5EYoqg2404~eeQd^ZMNQd)-2~2+qe{1 zV_Q5tqmM>}F`iC*`|OF6Z4tNW{eAC#w=+}|zT!q6;8l)dn=&F`qRgx4a)+eJE0d2I zQbR<^=gh-^9vjChhri6U#u~G>hR8!=y&<_TpXvf@4 zR$#m`1`eT7s)@F0*<*C%n@ie~Kr9Io1B|WB934+rNCj@x5voxfQZW(r@cNF}EPMfCxLi6cg$TxaSy3DwQKC?-fK9ou zbJJX0R}8O;Kn*2@B3{iCIATt{U|T3vK^Iu7Q+WVoq$p~o_`4M6jeIEQwBf*sv>BvO zxJzjrsV_yI^|~sx$=^sz>JYPV_!&J96!exynU0x<4Rmoe=u`lYo=fwXCd|mTA;i?8 z&LA4Qe5H3yC%Jjpi8C%Kpd5Tb5ugFnd1o0sB-5}m4_fij?5pR^g{F;&+>|y+Bn30y zEaF2NT~b_Xx|vd~)xa zz2-I_x4+qUy?oBgnlfArHT$lYPhnqw!y@$iAFvhsHjo)!_T6ky8Z;i?zV4LwjV~+9 zy_DhU*`mes=U#S^z1eCf-@|_xLFjVM45|b`ns0K4x(|SG4u?n~*|ujJ_h~2;zah{> zRdWUrMUImjeNN3|P^Sq$8&K#QkxUNvj6$+3~mmj#bIVEgsS9h z7?VNmG$jjB%pcN7Hqi1qD^E@mlAP+~$T4S6D%6r7&sT_eT2UvZqC5+y z4KU?&mcgnp$DsLWn3klmjqAEh4Q3jLQbYzz3K~+BE)=a0!<#gsXHo@}QJcVxa%_X5 zoQ3Ssik#`*#g-ZQbMRnefX?C}B$*Bo@x(UWUli-h7!ZfGHJsjqtric84T#g%tBuNC zth#KDOBS#U5ktp7v6q*7ZP}W#=UvcoOV7Q02Qn-C@Se$EO8GwaJ-1IbW4FF+aar>I zvX_^A?+aM={_>hz768r`mu<#=@b0BP{Iz=dTwAj3$9}<1zwiBJ-}|zI`sI61zTyQe zL(NjJEw|hZ0^4y79m|zrWaW_%C-g&Szy8SQ{pksl&wmm1^1Wy6x92l=+V-)V!^>cr z&>au}4M0Np)L{@6lv9AwP@uoCu!1JwAi-egvZ1zsG6DafKK`jc& zurLq6IQoJ?747i5C1W{e{Q|(fr%9cqc5j<9TR>&u4|W4~zyW)Jwc-$pfMqDy+Oh4{ zpZdrL;AKb|#aFWXGPW(*l3FXy{!i-cuYBt3U;e}g-~Fa{{K>0r#y;zdANT(9H_!Uq zcfN7DzhGXv=<*dy7hJzkevLnq@~=|vXhOIGKe=0;Ft3>ajxoT1vf0xTi_7q}Wlc>y zFfq#XGJsO+wr%FO2uqhO_nNar&~7Of(G~?t$+1!_r-&=U5yBN+@jlC^0l|Yenx+ zM3jj~X)@76oenz%vKE8GL8)AKI6*>TfTX!ek`0L5m2h$hXR!I!V~QjvQ>5C`dUvb!;|9^1%?^WSF6003H-=F$n3!sKP8kS8=8wP4uWNS1b4z z=K~Z%eb4}#AW#8AAzb|EkC=)Hgro(_z2 z3q=tflAs}vWd;hz5+cXrf~iv`Ky!>zhlZXFjBE~!X@&=gvs&5+%E1$YNcSzu)}2PvV^Y`BOf6-6H=Vcd2z` z>FO(dG5%swEiOaTmtT4=>}+qR6jX-Ue*juv=WVlx$5Ij=uhe#lZ4qmtTCrh3AHsF>n_zvJdrAAq= zO>%zFut^T2pf@EFsJ4a<6Xg&{A>X0N>>sWnVxj7l=Y^CX`f*G3?RnUnvhjm%2OhwenVSOi7LX57gFOow%auQgA zuDPUD{Di2@B!!3$QH$n4}Ja+=9s6Ni{9)60^2gjrHWV8fx1*oY1jh`%l< zM9c$n0#UqnAvZ3x#K2vI7!=+{9AY;r4Ey<^Q#+qzn_S!JL}RA1q_)&a)OA^?p(YLy ztCgpB9L7%<3Q>}Vh`m3ed8!F9X(u1U-^}`QS5&oKDivH{5y51*7+wa40cTsXQ5+&F zpuor$njJp$YV-P4eH{D2J6FTYmYDBbcMGI!m09xg@?EQM0FwjDlTT!?Uf|={_G2sD zv0|S6*kAv`2X@|Wi#dDmU_bUDFWp``Z13$MW#HG!vd?){u(;eC$_^oA3(!8J4Ncp3 zEmwXh9k}CB`)~i+BPZY1CTjl|ZoBUu+wVU6DVxK~ARdeizCi>+UyvE5gLYw3!9P0K zE9wrzqC<1a;Yx+dm_^#yAWFy%{KN(i!l#z8FpGWxRXpG!jc^|Tiw#8e-${aG(NRbi zbI6n^1&0A-1^84-qie6E3Y|1ct?nsglFKq(m{H(XX0#ZcKp=%K@DqN)Z;6@ZAOkFn z&T=3czbF^lWu%TET1tR|!FGJln-Ev9J| zir@j8L}v2Q$E_BF(-V(Vl_<8kDNFeA5bN$-RE***#zuR$X@%k+bK5$sO~12u+6lt4 zEc@(dJxjrux@$9uZynkR46Io-_N2o4sqbhRBWD4~=>)NfTEvLo-QTB}jff`jGH?wt zD}$vR$N)VRnSil~bp63rTPavM_33Gwmi=v+kDr^FLS?8Ba{-24=-u$c@PICGLfDFS zY@iAQnBk+}B%3X4gT#3QksBL0lhb?gdIZk<+bKx74_{}kCpl1R-4VlkXkfcFKkF}~ ztcl$bh-AV|!Ekefa67$CX?k5Qpj3yvE$Q+}A9_1byvGZly=vu>pa1LuTe9z1v+SM? zD{Z}AIR7H+&h}=%^Br%p?yPF%iff70U%4*)$AAAf9)p+J%rn;F$yCcZP>J42B8VUu z*zOF)@cI)@@QQ@BbzU+$A!P+~2ET$ipi_E5RMXNJai&_&G?bg|P>;)z>wrrW6_}ff zYB@Z&8!4W5dZH+sgyc}cD#Xqm)=K=JSZtUH#DmCtYBdRR63g?Bny`dDorZ+hS+!E`B3!Lr%c;{A4@D@n;6W#pDoOTAazLfI)!-cIqZ|lYuZUGhh3$OVjs%CK2+?7R`Z|ZQYctFNc>CSij^B>qTQ~ zfZP#0H8mEh8ejI;+5B9N!a}W#wO0JVJVX?O!bn66E??x-DFqq}RCJ`VM#P3;ToR3M z-IrI!j7PZ(CPny5`6MCACJ7$7G9nekGS~oRp;l@QilkOI^-h_csRWZMMPJiPzGh%`VX z6UaOQ6*f}DGlUerieMESA`XqyO2xY3UiL(!5nhuVY9cR;S7r|h4tYk$_0e!Euz>+= z(^JNnY#1bE43qi1?snG&sKm-?o+nm;KnM=8zF)iW>ctDLGC-z{S*@pp=RE~e5mCS< z>~ZO!j&K)zw9(#-^YfF z{h1W(?915xPs+Y)AN%q@Q;@QP4ciLWEV}B#i_X2+>ABzv=-BCOSBdl5Sua^xUa;`W z%jaDJEK3eu2Rtj|LyqE7vyOY^I@oAV(^a8k<6FJvbDR>-@DJR9SyEdfp;Rkn5)7`{ zfMTl13xeI?X6QYdBT^Zw$WGS|hXAdK>Av(+txxyhLN@i3bgjhiaMa*z$tT~gb z(NTzc#FWgMIA=Lo!`f;tWnnqJ9F?y$$$XGv_i^l7mR{`r<$KpI@_lT0 z+55{!9yr^_zAOsQdGXT^*=t)L@`8)OVaWH*$G*6Kj8c~1Xqeg>G&J3xPhoC7`mmR7 zbHMITJaYeMXn4RL+q~p?Tf@uyzG%nI;bl8E!?proNEUnm5EQ^SAQYVRk|WTE2dsu+ z(Ovfh3GFL}G6KRVgMlaz&&3cnbcMu;hgfZuVM#zkTndwF?n_p1B3%F}@EDM^)~tzj zRiF~AfQ4~t!XCC^R!cZ(0C-hhjZ>kyy|O5pPzOFcRL~p)!-!=qv2yR0q8BP#WyVIP zhLTq`)2RC?_0wq&OYyd&s)#@(kX5$UpeEm9>#hl**`|Lh#Wxf zE7@LHZcQ1{inH&4p@7vkV|#_!`ttkV`Gzl__EE`xY=6Go`^yXGoqyf@OT4`7wP=4S z1uqNR1gq}y-Ob(C3OJo0?s~-%Y6~sg1X0`&JCEE4QPkor0#PX8DXC3}=qqNNc-*z+ zu10Rza(b~9!_Y;thLVCqiAIT$T45$l(nP|8s4adfQsjw*=MNT?y|WDAdgIvtB3a}$ zH-x(Yg4|lFC1(4s!AX!=**m;=EJiy87)GElUeG8i03~2LpNZ=~yMagBykTmpE!x&c zkk<58MF<3{zy|BK-iWtOt($t?Wrww73ZAJDrSLhkN52qt7SU4|dIkUFi~nI3_}A8G zo2B^#LLo#72a$#oDm0}{a%kQ9vcwF_#~$Voh#4*u!*d!NsQ%Y~{s(6k9?uqB59tNy z2q8_!DRsd_cW&AjQ7c5KADd1&xwQ3{)BpUt-xzWJr$R@-Y6#c=TX8-g0_M*5?9@A_ zC)GBwQ>sU*Y$oR)-Q)++yJ_hRWrmT`DaH^>ILVq1r{Oev;Ul?n;XNTG{ICE;nhV<` z2J{p{!J&q(0G_8QlWBD1G;$Gv3L4UgUKJaR2se_f2{t@*;V~TH4ix^uP+xdVa)_=| zy*{j8tU^>pBe5)(sAG~tL!%0X>o9XQb#lhjLRWrhMr9u7*NWb>$3_vCD$5Bmo#e{o z<0LIJIHVNVWJ8_8lX)DXp}gT>HU@3gG8<6`)gop+t|tA9HmJrSfUo2-kAnw zrg79(ce+zMzwji7&8`52g+5%@VE1M4?M7JSYNY>&pUW>rGx)w7%?6vK3P} z9cpeVNX005QP>|HKX0RB;2-MH zQ5+VE7cnMQtx(@vv+fBw@S8zvOmolAfBtDh5+BJ#DZFAP9Tc&gP3 zFC~S-)N&M@_|aXhG8IKqasm@*-+5D>!5c!)U}N#qW^(Nvu0_ zyKIaMU)Lqm&v2{i{)kwtYfOurvx<;0g|aFNBH_cTJRbmIOeV?erUEEu0`oRxjkzjz zVi;n9>kGeo&R4#4`lT11hfUYCGAm4b-wIoJ)jSNe8zXvwM;2;JDW1t|(KGv#(Y4Yx zv+1H2W|w2;Z4;heMykRLOhNG^Gl^IYc~VZ1*~nmRxm{;b8j(CqNU1K!36%35tU*z1 z^2sSSgGMvBUQ>ZtZDTa;&vXqf3YsIPf-xwPLK@LCj7({0VS_|+!U-v{hcss3loT8y z)|?-tVcuxsWYIO8R1@e>l>!wJ2c4+&VnED1iXvi8rb5l-nh-8mj;=g9k|GpJ`8M|^ zJZ5FRkUNw*)Lcglv{5#c3M`3@sPLe}5<4|SX9kMHAuWL@^;9j%`B@p8G9I?kg6D63 z{#UMM-clb8@#vRC2xgXOsFobFS8~W6l#$|>=QP4yo#aW-P+>YzF;Y#2ggT`yO7d}P zODTsmI>SN*bhQc^N<)r96rr8ua^dPB6=Qc9>89Y+aczk{bCK~5(F7*TVPrY@+$uAG zJeHN+;(%rUuD*8VGB18UxM9tY?qC0-``12j*KK#*expB2zjMv94R_ph&$^rLUBCS9 zwM*BpzV7y0uUU2T{5x)1xMAgzwac%*apAeEmtFF}x&>c3^&LArb&Dep+F{N<+Z^=L zryjch4o4g~%Re%|e4m{T-+%V8FWdPIhwuL8qxO`){_q#P`RF~~aKx_19x&^;gLgV= z-)9`M=O4`3<4K3TWb32$d-^di+wSQ7x1F=w;|_SSqO{)J*!a=A|A{@ zA5H}cas+ZEORnG~Bxo5LPQ+lzl^hHbI>|t&h#FD`R54J2Sv;>QP)UL^=GL{tx!^6m zLRoE9;iryF6ei(Uu&a}_+G0bY3$rr#+#AYjwROlUJyMo4k~I7vt$t~xwkG_5hY%Es z%%Lx3kz1p!3V0(+;1kO@)zDW+1!BWhB1o9@;eMHS{wE@kIefiYrqO~@%> zivf|e+J>{llhtBKox=o1kxLpDkQ0bOTk7P>Lxn?8wBhqwE;Q%sYF_5`;;jw9=r~;> z6f7)S-+a!qCkxOpGHeSw+aYg?x;_vRJb?w6;m{uMUibw+cbPq#Gys9wK_nL$i=LXi z=2fq<3mW<+7N>_Q%oasTx?I$%)g+joK^$W2AlT>{HdXWj5k|xqDT}%dkX2w}&3p{- z0y&t8(pFfD;xRsf?AJlLsr#;XyaS#^v?NWSscyuv1;DY9<_%@1Fwdx^QAZ?T4K~p) z4(DXl%tCgka!F10mtmFrRP>!(!6~Y$Z)JOeO|S#%ai?W=u(cDMG|RS5u2* zefM+%T{+dN_zh7ncuy;iGA`)NR@zG1O2p!4E#dJn04J+DAJI`v zCr*%9(lkKVeHv@7fKsnJLCR4Ir>jL|5(xl18>q-L%*fRI!+3>Z8%1n$$grw3L>ZRT z&b=g{Cqa4oFlm}FfJu~U+la)5f{vnt4C7BS=^Z^%K#>&C%|^P6uW4rVNpr8)+@YQ- zNHIH{s3RPHoLOuFmaQ^NK=co-yZMU3%v;P8m^||Zu-2P|K&9zWQ$Q<*7wvm_6J8cX ziR4kHXBuOqw99GNan|ylT$L^q+h|S{ErJ#~QKF{JB~ef@PoYskQF1iNNO4gikP}MB zr7pjt%yrX=yD&Ig+24ez7QGa4RK-7uqh{L%oFwpOoWa_blAWa4%Ac3qiwOA zj?@=)wVI&o6gg6O2L&$1#!+ERVI=-_(Tmad1hOh8ppPJ&B6NquqT?hPF}EaHtCGW%j>BmN ztqhs-+PD}mBdE+PrdF}8T7@3Tx=Oo-(?voCYhIA7K>kG9!!dk3EMzCc*q~hepEri( zI8hoMW6H^@hP4`EAQ>IXn197(*Dqcetd6jagK%x1P;}i#GeFWc1VbF z1!aZY6(=`J_{mIft)>rAG)!;wQ6@RdF_ct|TFjtdmJG|YA(=H%&dG+7mz2>7L1$zQ z2|-a6Pa}d&VR!(uga@TQRy8E(DU-}1Y^um~!MXVSbG@^Vo?&T}Q-l+&mK#c(X0>E; zYKtLG!lx4=JBLrILaC=CA5BzECuV5d@F`2Rjh}F;38^ZBRVj=nNgHxp(o5YhTu(-F zk?AgsGQ|TvKa`;6z_LYWo3X>oEiMndeE*%R9=L0D>E3lKHr&44Kfl4t_pH15-u1V@ z%XhE!LF^@WtXu>v-+JSfH!qsIdg;}7+;r9QMdz)!?))`3U3kW4-?sbgtq(t7`-As> z8pwR)K|39N$ZX5a@bY2%?R?CBJH6)MU5-2Gxkv5utRwe+<||*e)6x6waOjJ-Ire}Z zkKS+lBldpUEB1Z*u?KGdnnRu?9XDsk!}ok5$b872PlA^Ze9_iNAMotM_L)6<`^Rq% zFZ+j9m>2W|_X4@f)<#9t(4X&6iN=*-MytRUk)cIw&@Q+KfRqE9Ag53nML>=@NC;@O zsEQ6VLYLU&z~=wu>pp-!t*Qk7H#tfW6(k2q4N4A8H*}!MIV(vrN)iMK5+oQv)KNhl zMHB>OKoFEHIZ6;m-PzsR+O65$Nsi9w?9A@$)<4vL|DX5$%B^2@!@hOu*16}NbkBXB z=RN0lpI}I;tjN{c<5Nr0%(NO)HLYsq1GTgnVnsEL8IK`sQu?gWhDafbT$e05V3B4m z2C{}Y57DZfxq(MlE4OC|1Du|QP;v$x!h|T93umwuhvvdJE*YW+B1()3h5b8op%C!I zD9(Oifas>}mOuTI4>iCMw&bNu|3k$b@#{=3v&3xreU-s&-Yc9X^V>X?? z_SGAzhgoVdVX2S z?2}SI`0m$iF+X_koxZ>P^rQECidh9Amy;69rER&2s-G;>%l;jYOx1tnvMFbf1PtyB z(v}k;by2!QuWxi4A;M??(*h)bZzsvxYLX9OJiN@gp?)jW}Y$m)d!KchTx z$1*l|Tstt7U2L_jCQDIB3ZrMAh14C<*xV8Ab`D944QR}20bUUiB{8QNE|_3a~c@6^UIJRV-q(orXy!zOrD3qI# zDO&}$;FiGM-5pfT-89)!J&Czv(hzZ~s3jG^(c>J&2-6)2MgtUV=3Yp(Bc6iL!eQvg z6CykWMY4Js4`=d$9^yi$tg{m;Vy4q5uXxCuDWn0NIi92N&~$*%QLd$y9z#R`&7_C` zY#R-14GOeD=p5LQ)P5O{{eFh4>zcCb}RGwz&$=>l%b-8!jpU}>@gq~ zLslphtJLD5dB`SCge^xJFc->+=W_u}Gby8(jTRzdf#*MdwoDMK+DoSFG=;EGHYRR(V*Osz+rrX?6HKVAJ@wiaB9}=A&vfN;W zAVai~wWmp8j-{N=GWiyTAizIh&`IUP$Yaxtfe1q~Ri^3ZNv2UiY#0LFNf>kp6X1`O zH@`P+IV=jWOjf-AWr&Jpts~!JdHRMFpLs$94^4_Q?N3d{9}f>j8$aKK4yAze8Gjmt zg_Bh2f~2&FhO%Weo{j`(ilZHU4Y66I0b3JkE-@4_cs`SbYjwj@l2;8);*&h0R}j)% zFqsaCsEth=3<26w$dJ+4;EQ+J%G}^d7I8QUryqkM5w^-e+|q^!k7yKb`uQ*{W~DYm zq;!Q>i=Vg_f3iaybI{7d@nge~b=?$B+A26q(3aW}E@!#}n>fKqHj55AoOlJ%70-xc zl?eu>DNS+c77&+u^^e<%a*XDnOzRO7eN~SLR(+@~$Z6$k~y&n<) zMAz*hJ-Mt#RxDR9qd>fW;8(9buVnTi9`6=@>1nrKKWu;bwHF`z^H-mG<7ZE(mw)!$ zgD*b)qi275&r^@yrCxsVv3p5pDRdHD99-1U{aZoT%Rvk#oP(UJ>htyR6e_wF0+ zH*XW4V^%NE-+9Bs=WTJs{4JHt>g9zyym#MS)>AL(?Kkj#?6<3zCH!hqrJoX5edoSn^__Ch>xq;V zHka)d*)3IdYO6_&y5Csohp6-eHepGjs4Eqrb(M{VFjw%Z8I86bOb}hGELAwGm#eRh zD#j}(RlX26IV$IKiL0dYQaK(WjdB4(i$=BJh*PWp*c3x}AOp+FU^M&4z|t=xcp z;&F%jSgi{B8#mwhg&VH6?ayeQk@`F8j?HhcF@GC!#)r(8*g-nmcEXhxC=e7*sd3aT{9s4EPconIT+0?KD0a@d^xkyE{T zOJM)$AAh6lrIR+`x=Ig=oPo2FA&bt67z(LU+U!g`2XF%J=|Z?I=KM)vQ`0uee+cHJ z5;RKT;2g}N!LBZTf^g38Eyw{*k}bk;aYN?-W`@uK4GhVAO;gD!m^4#}uHwdE$^d^R z3?b5np%p=)p;d$Phf&a^;$fG2*m77L1i|^pD5PV?XsYng&uH!y5JP}YqeC;7Ootvj z{?*HA=m=pkbNPT79Rr1ibn}N5hbYKg`kBBMU;~tpN-!&krimw==^?J`aIz4l<}A== z2;HHM4MH0tS#Drv4pH#&a}tp_RoFUa1dtw_96(VotAf?Sz*~U4{72X3vJK@5qG9@pdGi<)0|r;AC|cS7Xlq;dyRTVic6}MQj#kL^GX|&!$YXg(*0 zMWNOW*b+bd1ZmJx?nX*(7!q*%o86?%3UfyB&|D3U4bVed{=+F$V$1f~3J}vz2@Tp1 z3Na5rq$h}>Ig)LC4Mj1ULhXPeU?^ZHTlqFMyXkYVb>SVbnr3e|xiis_W6#fh&XK9n zH#I=GstRz(0SZw@{B`+Yj(7m;aG?`bxKQLOHr<%>85^&%0%?vA5Sx{9f3>?Qlp$gs z2x&(Po&Z11T0@L#vzUjulyYpMO%KEjXI2J-nkV37YEM|20Pl-|8G~@{$bd(fE z93o?7W5W{zWRl-#6Bj)qoC$Qhu;`8zBnZc5h-p8d8FlV+-c|a0-?>HnS&0oN-?W%r zH-w8JLx^OXAO!yaU4SQGNPM&yimlNRVMN_bu#!1~DH&~oNlmu+X=v_{1R;1@J~${o zh%Oqg>cHiFTw7d2q%sg%H+0~E0)#Eh~1JsSugkevYWBJSlC_JFFg5> zdfEQ+FJ5`_SFb-aT`WKT%JUDs^o&nO-K$=PHedf8Z&xoX z^ppl_!zw-{v(i;rCoz^Dd+U*J&?xj2cd9&fpQ=+yXtN1L^5xR5oUF?t!FR$@`~(>&OP1Bu|2A+C=Psf*|5jPFxsNR~dDKXl z7de$m1WVYLwXb=MkmGbFLFlHh5|Z^%S)rs z$)&6)$J{ZiweLx%k2)!1-BC?6E9DF+OgLj7$tJrBk!B`bI{3!4 zr+w|-+A~#AO3T{-r9l`eRFY^R*V@V^Tul-1lxYAf&9IPzw=5v%_i1tYnTMGvXmOb}{N&|qSU?_BAD0=8E0sj2aH4z@tnFCFM6pB$I z^oS4KG{uKYh*?C$)&$XU#ve|hDPYiru*g-Kr>eYZhnqDK6i1E-x5`|_K#)a1KMhS5 zGd($ zK(nJzGDa3SRirm8%4AB77H@eX=PAW1%hHVqStPB}&zbpyei z6meYPK|#l$r3!^k*Qfi$ZEypDJT*{Lu!-`Q>EU56BJ0FNwLp8zL z=7IsF2L;~Jn-^l5X|rU4@5JeM32cZ)EgWDGvwCfm9vEmaWi?BVF^Y%J1mQ%O)AFsz z)H-1y#33|re(1zWgCTC`PU@Vrm6bxIEsTtY>ttT#cX zIX@szaFTMR(hAq8mveP^OHUqhgSLVU$sA;GPC9^Y1zQf{5LS}yjpFWM;5It($x(QK zUp&WAnm7%JeC|31cL3mLnnl01*}P60en`$Up^JeyuK;A&Z3=bKaRNL7P6HEgJf=?@ z2K3fK7ab2Gz?M|qxt1$f1+NtzZ|^u%>5o- ze&YT+o_ydo`^!JQ|C{&R_L)y!aQK`Z-nDS{+KT0!wq9x0wrlLZ;t?Y%|>QB;UpRiqXhqEfN)P(7GtL^UHIj!m_zEXAxubvL;0-FVbjSqfB? zs#HU5%N~8i!3?QEJZme(Z}G#DZxo0wUWuAK_NXcqZ3r~rkN)wX@=9Rb}%)}aI-cAN)Qu=2#7cf6x=?t zS3zF-iF4Lld-YXTT2k%Y=CT_3=CAl3)+Las+pupC)z2%R)xzH5^@;P&@P2H2%AQ%a zn{3nBgUsq>1@i6R`l^!I*0Ty)d(#)M{N_#9ee$ByK6>gA*IoIs8$WYN-;=uimOr`g zu3H}d(Vadi<^9<1#P-Us=bxCmp;Z3to@B+NEt^tPnO^yxf5HAi%e6rNsi#uVH}hxz znw>#GwU2+?C0Zk4!z0}y?q7MiajIyO6~jx(Le!fOMhy+nZ8SspbIG}k!c`22ETS}| zEotYEiB!X4@PaAnH2*otD+NdihBRBr)BdSU18xhI0~o4sX=X*&zA6f}5@1iE+N74Z zrvyJevSKDwmo7MfhZM4f?!cDkQ=-dZQhd`Db|q@LYt!dH1~{H(M^8R@q53jQF6ltE z!5zg+ffAnE=0#9f*~=CWX|`l3q(l(-$wLTc6R~joVVyP(3qtWxlKYOc$GW{I&HK=t zBmBqX1ayze{5@v5(H(_2DSj$jm}Y`F&($w}tiW%Gt0=ZL8PfH`g|gbbw0x)t!a$r? zcz6rZ#Y4jYbQBtxfE30=5KYRWqy!NVOvIFn5<#3~3kV+NKQ^NorPCDJ`!>owT|^ql zHnm(elprDmGfn`zKhwCm)QC zIWhz%(%gp|#HrQdcTa@_IOSwh&N15HL?{6eT||b87)qlkjwsrK_-k*jupC*;~^T7AEFp7XTxJU5u1*ozz|B& z#!zBOgMv%q+C9CVW%lweNi1y&E&+u6)nEQc_+5A0Hh1=}%Pu+fS;qC(UejkSq?DK? zz*Na6bJZ+R0715_GJlxn1BW?)6JX2vf=`1{L@S6hKXY=-my#IDesyV}zeOiP!taGJO}MVJ1GKDG*=kPxwmDr?nrS{~0+DGxk1k4v z80D>yDsOo^qSR#99F>%a7A-upj0vbXrybcw=GL3LiWv0yW#Nu|Gm>|H3LIWN! zLTzZ$95-4cJp6>4P880Nih&7T)6WE+T;&Noz)VR2UnJ(FjE*5}#Pfu#E{Gvy5rqWb zj?O)!#So8foZ8H3Y0hoL03{RL0F!h>qYEKk1jMA($1hOXML{oaIO!w~@Q92>F~_05 zp~;qh-8@-j%DN_{Wyq1-yr=?`oO!5?f5f?2H_FKXF^gK1bh6^m>Ui^*jC=kax18r# z%rxio=(0izkKb?{X2jtr=z&>_i7N~SL+X7L7ZyB_V!|0d&{?byhlVlD3>l(CFd6Ek z=qDg-7)C(&xyUpN6o!=RGFK4b=Jhkr1TG)}djAV_ zUHrS?sFyvqJYI<1o!D(R8){Ekiw=~`KY!_|0g7cM^UKdZ^y1S$@xHHVc+maWo?jkd zf7$cPPyP7zXCAuksh@oNfgjv>#f3-i{QgxuzdY*YJ$714z3k!TeRtp7ZPy8*p}jUf>)2gCcH+F#j-GYe z(Q}U6XUG3Tz3fp_NNPSj9&QCr}A0 z6&00ulz~c46wHcKl_UNH@vCZ?!=NH_uXSIyaXV{;ssfWOda$WRZD#rYj(XE$q`uVC z1JpG0fl(zYz@~a7r6RWBOLKIp46(?6zE#5OBGS;k*#vXmJ-VEv0TAZ^6FkQTX?A4& zsz3xl2y!w!wN>K8Ib*&mZhEweaWO&UhEdvxQ%efPmlV{<-tqOJqYv`puLmEv%U1Gd zu6573N3gFq>K^R2o4<6UZS>1j$4X^pBzsrE3=Y(mIul_{r@fhG$g0R=4_I5m3Q@zTRTp@&Nyco3c7rZNZR~QFFiwn zJ0<;RHv~~H8$yhEl0OkVslq^ta^Av+R5**~f|&!{R!j279I;izkp(8g1apFb%^?cG zV2+Ahl$c#8sHF{so_339Q+f!U!qAY+L@SNg5@!f5q$+e22tO&8IXB8E3J0XqG#CCMGG4H(a7HeC{K4$G{W!60j?)Nn2sBLN-wsxY;E^)Di^mBn}@}FiN z@}c>fvbhj14qSpXW&D5!^Ci_v!6B>^A`EaqDvF7S2ImvR4<`8y0X!Ya1Ti>HL{Bu( z{lvi^&ci*ajln3-c?IVQz?=!o!B7VeD-g^mI6*$t|4Mx&(CQ@h0hQs;1-2%)bgCT$yyoYKgPLXruTMb+Msn;by@NM_s--BszezjvoU-%qC*8V(x0A@N_c_X2&NF zjfx&ldT7HBT5CfXyct`i3?QIgW{)%Xz%q)$xgy%;GNSZmK)H(K#9@l43zv|{%Bq(o zLLB2T2h5^1<+-)vMqNjgc}1}nE=+P5O6ZiB(VzyDx}dxzWRdB-f?z`&zLgP`1!?)i z3V=-FoDr5*LBz&&yJJslt$`y4ba*3%I|~MIl||NF<|>ZN$Sh=x=rXjDwA|Km+bAFs z11HhTFd>M+5HUfcfXE~Wm)okPg%aHgXE$B{(?9$@L8~l*lMHo)|Sbv z^{&;IUwVn1w)=orPT68sTuXgv<};W0pjfE38gnr^GR&z=a=KVX#K8YCO|k`nhtVlC z1@6hKfvwUsBN7i%$W@Sr;!umHfG}!;qh5wnMHU-|sbIeO-@(#2yq$wuc*_m9h@B}; z5Z5$5@Rpu@W*x#+lVX%w%8hE#Sw~?-l*|`1Iw&k001=5G(lDmO5W#U&Sfkbs0t`{; z#8HL_TBVem{t5~#As5JbVU4>PE%_-eH$=h^Tj`4(xve9@I0U= z2kPQUc}PLfB4XgPn8eQnx+8&x!5n8Muwl*<_#%*9qj?3vfEaz?6avJ-xrqP7IaWM# zgDhqu$qsRXIE|t-3*qVn)OPm*1P}3ehW{vlhSsi_V+Qb683X0S#l+=;LSDIOIBq$E z2WQOmaFwd`5Xsf7FiHwT4liQSD4;NBjFMW!PZZ(-AMn5n2oK>b<^w#pjmD6|x`twl zC$%V(#4hZv7=bHE;7ZcP#J>`$9l+^7f)d!1%Y96$4P|vPiXz%A*`r=I6nK8w%dtJe ztZE+hvXa^UvZt7PKlbY{c~|yS)og$HS1&#O`ZEu{`qX{TJ@UQB?)%1lcU*t*xd(5# z;S%$9d5?M-ESR;fN0>do>}G8H%ZKi@5^V}A# z57eri)vzi%HKu1g`)I6MQj)B=Qy@!~6@VD36WfGU0Jc@CV6>g8@KhD52Wdm6RxYi6 zR7zJfs#r0&IaVS3fvq;1x%uYqXmzuzQ3}igxW|9)xfz>UK9K510Q7L;>|#deFgI`#^s@jWJxrtMs*aV$9#8fl^W8tV)!VSI z|BO4;KkKe+&oAp%O=Cl}*X$l_m9qP;zjf1%_L>#THkW;E+2*njF)NwfuB?_QK}&Zf^O7$Awf$d~fH<2V{%hk&9A2X3{3>&ECZhO< zZb<7i)CDy6?;f!I4lcb#T_DN;6K&EOsoHWEGn)y#j zwwObtkhrQ%Hg3zjC)o7yo8y>;9}v)Wh13Ljq8g=K zB~G?g&TVuItr{hCqI0GwK@0`MLn}f#2A<;wG#e#OIe=@8q8sG~kpM*yFc8OYnc#eI z=Kwccqr5WR#V3EPoe=R>7eAwvm?ofv3DYlhD0m7xwj!Tq8W;lTR1p+OvXIzpwJbZK@R(nh%{V;i#w;g4Cs8EUOb;qQ1h1 zorgt9Kn9!mIA{S^5#t};(utzmL>w4+tO$B>Hp&p9_0pmY?jgZ1=<^W%@tJQ|jVABc(d_wPkITMe1eS%}VBq<=0<)(u2$%URE-@8QaUg zx+~jf)qnZglfQW7G4--rvY&h6htEBHcklbMzx?n$H``yn@}i@**=#v4`%)~=^|I>) zn;x?Fmiy1!T)o`$%j)HQcV1t~tXQ7E?R&f&+b!9L&fVzn`J1>I+b!9)l@FW0!Lj>q zamwM_o_)-Ym!3BNi&q}^Cs&^xa!=uDdsOMCLR3o{RRE$WGigu{0>v_-dRZatF=V^9*xG=W zGk9`&^;K4oj>^56z`zY9DmPTbUb=hm0ejoMato+!Xi1R*nX3>+6}(Dj<^U54Rh|>1 zM+T+fRXa1v9A>V<@z^oc?WS%#6q4nY8oy0;qgCAIOQ9iCM&T9B+=CM|-;C190bLHW zqKgeMXKxyX90oMRNM_XODP)%6q-k!k$@%L+m5lT<$Ycw|?s@N@+E< zy=Gg?3S`wWP$t_=*3~MFRm#BDvr5@^vm3Luc&`5BS)ab@JkKv5e&C#see}4`fBIte zvYWAeObWOk`_5Z$ax?Y=ci-*+_GCd7r4mn;-o}%hQ_5$(xMxRcTU_qr&M{@us3ccwHv&!~o$oAR?I*D?AuT2@?p+# zWmGM13Y_ut#XtYyw=~ehVJ5T=4@#I8cD@->yffiP0AVyi)4{3h;CJy^XRS5-Z0L(! z9!|+FDZnVrC{&3I%@Eqia*RSaN`P$usr(!hEULVs6Pwm7xPi_||C`JOZXukKDB^(4 zFNc0$bo{VLq}4&k%qRzn408ZQ7biMbfo{G*0J<(tJOfOGTE~nKvEC0a*cfGzA$}6< zU{1M7bsSF8Igf|7LZ{7K^Cie_#2mwkuFJH^vaV$@B{^x7Hc}j5(UqBp0*->sC=K** z7@#oM)Y5E2c|e7;&E;N>tzHg1(X495_N>Ky{u$8zTIsh;3CK^RHz*Oyov?8!2h6C0 zK^GKf?m2gyuvU|k!bp$3Gx?I6guQvyp((kN3xTq}tf)9B2P37#=_b{PcH*#6HBuvq zP#GQ5#mX^ebp(os0>U($4}OPn_X47G$MJ?pgrB6WPR`?ph(976Ky(phiR`jfu?#`6 zImi?Aa#0~dHyzWIn;~)=Kc7q3aKdvw&vTwCg=aUq%OOn}k8kw~jv~}8mpg2;_4~Kn zY~8iq?OtoJ#;PlSVCLqZ|Lk>Y)&Ki{{`WoS&QVHB7iR6Wy<4)u*=L^i*Z=uH{o!}N zv8KGT(X3^3!%X%ovB5iJRXPuJkgy6V4SuPONn9~TRzLAYxP_DRFyx| zz+BKm41Z$KGGu7@#@i0{+)e|YywW;)5fu)ONgy~yn3ZISH#>BPfiCfqd2~dFM{eK; z!vUVFIHE+PldW7$tl*mTL7 zujz9vo>9}qPYGKCvVxf)oDY))U~KVZ0$ZWTNkm;ZPJ%e!5|~a9o8r`5NI6?w>$qx^ zT?|b>ot(!^+nCyL#spnmi5q1U1OF+6kXqb<^rQ;gh>W8Mu$~AcPD(P$B?u^#F8ltgVspQWO1aNS*;7^^R}|Y~_C&LXmtT0+eUkRw{C4d%o?e^tc>Bw~CuNKI zjhCPFNhx2F0>A$Gvp})z`^$=D@W#&`{nhJF*k69-$p@Z&=+0N4zUTQzZ-3?KJ5E1- zckjpcC8_zluIVexHkW-;%Kq}+vp01=w(m)K<<|)Z?BEm3M=aRF`@ZZi&;7tV7w+(G z;ALOx<)il6M6rC@k=vhl;+*r2-Q^3H9(Cj8AG-MTea`sc>|^%Z#`l*GSg_qD>#XMc z%RU0T#1hlrUtVHa-(Ox~<<*sn3P4|2c8{gItaa6i3cBvcR?m8BSlK9_ROlH}9x5Bv zyvk(tpcY+)s1j5XDg$vERVTKwYX}9{z*S0PnBG*q)}FGh?b*BTptx31wkzF!E@oET zII8^g*z(FNEUkFgWkU5zIh}mA!yCUnab3l-=b*_lp;lFm;$)N(qlo-rh0m4JJV&f_ zRnO9lQst}CHz^b%CTm_y8^XXb8VqSinamcRYG=xUE+4Q7snoq$BjUuyNp@@1#Ws;u z%65=Hea)o`XU{JyncahJyIGm6UcT`BGf`|b<5V5nTn3OyT$v2K=F1J+71ZkGDW2;u z^?vO0&pP(FqxQP^{8PN|%lD+b@9R%)xYAp)AGrIw_uTQ#m!5v)@t@q|W^6D1k{c@) zfV@uPBcUp7mb*e^eVr_2tr9{!aHkLYNW+|55~`s8;D#`lY?t@;Z{5Fg2q6BJ`PgJ8 zFo~D5F@Z=4fJ|zbOzODTC|AuW;13vnlI4R58a1irj-jr^8#Gx;unkpBUvO|TL?P3F z!t6FjUE0#XP}a4D6onop;z6WGTh33L5Yd1Ve{Pc!QWq|lGC1Wpb#nk`zH!X5T2RVS z-VTI_ASrI=tx1hIkfjHt1oHsSu%0=Ba2j+|rHh!ujYeUMh9Z{fr10oEgmR`Sz&l{* z!Nvzb|FGrwG(iqKbnRtx+XMIt0XLkh0_b#|hmcZ&;Z!DQtEW^7zMbv)Yu!Km!{6CW z_O`=!zw;en^Y?AwQs9R*F0obUI56x|+bj@S$xqJ10dI%r)WaHz~{K2P-_e;dehZR~lG>a4cK2!dSZ)rxin+)8KZKHYIdToE%o^qSZDrMj?XVo+w3BPnp{;zZ@ zNt0d3G-M-^8I+MA=}OPI!GCsH=MRekpL4)b=k62$F=Pvp5}DywzjUK=&%?H=3?DzD zjb=kiunM`bazm_BXhRVJxh(dv%t z1$)f)CWWmwf8VAXt^e-TS5Yr-zR89wF1wU>qewm}+#doYiAu>XhhwP zI-{`}D%H=B`3_Yh?2N+Eiv}l6Im+M(wr=23SB7P5?oCBXYG>WUz`8AF8;13J*wQtr23we zk{O^VlV5(}r&>GCUc~t7%kC0n=R(W5?}!-Nz1uTyn;~ zXP+?dlp}Zh@ZodMJn^9Uvp%3+wq?A`GD|P9#8NY+>*ZxvSVak`V)a!T4?#tr@Cb9y zFS`xf&Dg%beDC*fl^$0w%XmwiB=hQy65#&-PNhlHrJob0Bv$SuiE>VXkT{)C?Q1yK za8g1)g~Tx!1&L&f9&}JL)+rg=A}yYA3PVOZM`Ut{GkIH%H#ApMXi)+db*ObQ7928c>L6I51@!i>jg*Q0OfEZ1e-WI5ky@G=kuS3e)jYeoR^Zhra8rmAwh?t z>cHGhgdiY7rvSQ2ti9$t-Gl9GVh8QNmrD8AV-DYcp9KnM)+xlsPqNu%6izd`)D+|x zPY}gW7Z3pu(=l0r#mO!QsMTd!+i}GioMb7-uhpfWLJXalh!l2jHk?k~h}7~e%|sB3 zVe>Tv;P_cAe})V?&zl`tzFGH<8Hb;)QGyg2#YTAn9Bn+hMlAt=@Lx+77^0z2(9?m$ ze*w{=4^@QQz_A2)2q1!1 z+#Cje#&LFVo0G*557`#c4^2ayAiN+?O=`N(ke=~2-{8V!UUqhXoSb`rd#XhU58X`8xBvRje-JV5X~&)sTbyKFKFI{5tT4(nhxrM| zUsSQ7Xp1mGv%uh24)^5SL1xH{xVPDA3(olU>Rv!gJJOn|RGtGd`jIz}nwb{ki za$qPptfwJaEqY2wVb1v>MG($;ZL18ypAU^{hfa7psclq<5;5!c_@=V@iAq1m*d4NI zizk0Zv~$C%wc?{Y^~=)q<}&L!prtTzK1}LQbE7e{$g6@g>08=Tg+C7yEL??}aura_ z#1XmYFhlPgA(DOwXCiWbc!i$Yff%AEfP*wdXA45lh!Rc;!K6rnlkMaj*&066GbTRi zL)RsSOa~6&ti-c;yND7DIXqc@;t_BW^D`rVAPx)+==iy06b{Hb>XPK zi8rZyGeij-(%DLixlG_RRD9xRXcCIKcG6;;Nzs%3q|%1i`Ob4jJ9?W4J!8rcvf}CJ zC6X;Jah0HLp&^|^N%5Vn|j&50GAq96_-!{$4drWVbsN=SXM7rGPlvJOa|`3 zRwh?0+jRanKYy`LFuNuD^%sAtUjF54Pup33$wSI7JnDYzU%md6`>{R3{Mw5T0WbUV zitIO@`|%sk|LBn)e(mx5Z$9m~Io|iR&mQaTH*bRj7Hs6>%hk*6FS{Ar$Cs7N!2UAu zvM=@W&YLc`<0i{&yWv#HJZI|_J-KXi`RelzzUJKhKYji|*M9ua%g;Jcz5L z(RFdn6f~T2kexZ4X*@s!~=s+lNJ|mQ*JyFqMb)dA-`J zEmvi=eOUKYQ>d;~4og`f>PnR2pJ77k9reDpgt%x>XAF(pWQ8F8X1J<=BZkqYo zWYO6*O$a4yQA$(~}?s+R){c9z>*_P#Im@|QpV$y1I$P`&KoWngpJ{n)<0 z{Da$W_A#mZ|Li-D+<%u_vX#sbsi}I|W)?&`uJH<+gNf}kJ$o%ohd!>s+;w+yWT}YW54Q~mgsFf;61~|h#KxZ0w6=0*8A==A5%0>xae z7E&z`@aXgq!JJw+DQy=tbZmgm*yLy!h|~F&K)GOgdcDWA)l(9CoJplhe6Z2kER1r# zX|`B~>gH8#Y=#E376LUczlQimKW|AHLLmsz8Zskof}DW^Ji{(;xvJ~J{`=qktrM*i z#~0)s%+%VVwX*Jmbx#paN-(2Sg6>+h*1K2pD98W)fB%>3q2K7dY0y>9IqUXpBHWm? zTR?~lNM6CY#QCz;JjaG^Uce|EbG53jvmgrG1Rc+!2LW6nVxt+6&Vt~u;E7{{3~w_yZ6Zb? zi9mE&1Q6@R3TDor7}aHts{w_0TrMo1A5O=SkXJbUi04O9%gO-Se7{rYAl)jBYs`b=a(b+-#GB_S;)2tz=fu(nH*6u_ewi<`~T`oL3dp zIEnD8Y?FwgpbVBCoLaJe^x)oiZ~4=$x7hTZtE^-l?RT&KjuqeY&Q)EA-nH604%~My zyUdqga*^LgeC3KBN}6MfDBa~A3ZT%ab}+0R_Jox)WoD;6}VlLM6XHVA)YD-W^|))KnZ~G zb5bWmeCQ_*$3vsTdR_dGT!l1EZKKrU*UhM3Lab~Kb9JmE=3sb;r-kU48Gh;-n)D%~ z+(3*CG8l$G7>x3Vh5&9-(XkO}^&uFvG+PA#iAV$tD7%L?cJ@bBJGFWX#JEc>3+&z^n6^UEGy zRxf*sS>gO|-gpN5@-<(Wdi3WnKcao%(I39?+{3?k{wJ?J^TUVkzDd1&>anx8+hPU9 z^8WKTR4?0K_Wk7ryKdm&WhJvR*_*MaiskuR9)G}gCmpiW5eqh3u-&`89Q%OX)?K*c zyXI`Y(){gKRWExd_Qhu`{M?0ye(LOfrwZru4?pYpxyK!}!*K`CKK_UW`^?>OxuuqL z>7M>ua;c?PP>0P}YWWk6J*utbdAskbv{VDy3{@D~*p)zfh)*qSzg0o1{8T))UyMfu zZ6j4-i>MY>fsz6=ctzNUk3UAWXpdNJNDxK-S%ImvRBTq6Dj3z3y6Rl@sRCBYN>F+0 z25ctypq8IAYe!a?P$1g~R&r8ef4RDthW(Cx6f_*&a+*9jlks!a!Z@1z(8Wo?WqwV$)e!?dfLUlTt5xc=^bK_qgoB z(?5C98Q%2ut(&jE@iUj*eaAN*ec%U5=DWZ5E$_!xFS{jMlBtxFi^?-)u%*6`@?oi< zf4(J4hJdcD3F6%G-&uYoA(WTuVlz~p3@3|C9*al%N}*O4kI{ez3IQZaRu*rKGU3d1 z3Nt|`kdaQG!hiHI$JoUz6{IA!a5Dn<(~J|5GhC9TS6it*ASm?bm=-h<2wP2)?4+$c zuf-hb4khsFg{+s`3y3KQ-DGeu5T_g+6fSybU=+m^FcdNLvRP8OM6gNWG+%gLE#Atw z5dHY$M8j#{Q%f_V=@Vp#HUQx#dedpWbTPA=Z}0(e@+Y8;5~BopTL2NM#gGO=S+~Bf z(z=6zwtT>kh+h{{WrWiW*gmf9YV*F0*7f#-xpQ`r?aL}fl}>AFYW--#y0EB86)$Gw z0_b)cSCUm%Ue067-u`Wkn4{do!i99iVa%6Bnw>I~b5&(6l0?M893e!Kf>VOul#L=1 z;XGC7JOR!fUH_X<91s1sqKB*KMoE$7El6Q49+ZCOB1o0^P=YS1hS(~+^klKPVbIkA z6g)WbPx?$x%tj$JYHkX)ja+?No4B*z((zq6R zxtXZqOpq)d??7{<;XOY)-_&KhGLWpw7P{$++#Hb^gZvx9( zJn)v)?1B_pWgh03R`vxTz@+>L;00VZUJ*-ZIVNZW#1JP962C+g*HbZk^KMdz3{ZHZ z82X3b{EZJZy0_W?Z+ER5^1BHW!r~He$9KPN>sjK*5Q`oke&2@coqx_pz4gnj+9GTR z-|cZ?g@K1MOyS}{BGpZKg;KqYxone%3AQM8QFsOSa$(cX9^L9@+ozr$pXnf4OxZtyQzE zPNL>Q7SK#jK$TFBy2B8PJRb#JpmY;4G+fHbw84c!KrEs~5fJO*1Tk4d?2>9mO_mhX zlX1|+qs5jvI9Y(6hg=#(LHzU>YF-eo4sQ*iCz8*mgThIR>i}@faMgjTeC8+3lNxa5 zfS4Ed{Le#PwS2=3=7#?`W6myBGzV}*qfISNj-@tE41hm$4z-II!hn)gNH{t}6at~h zoK_sv%lTl4a{NO%H-=zkHIYWqA-c2`B9#13YOuwrI{<|sqbP}>=njV$LDHZV94jKi z1Q~4!GM8g0PF16{aSSpmYB z#1OVtVbbW3%5BQ=fUF?$hiq{(5vPeE8W15`Jh3^3{a{F8hKj9;V~d#G#oILGNp?xO z{=X%-lK6+^ADMp#a2FGo6c^FAtCv03tWxf7ZST-_W466!#5R|ESJF#QKjeE--t^V` zz96r?IQ0o;ulaiB@w>HCUtxaiUi-^0J%0BKkKOs`yX&~FJHZ^_3-li zUDuhr)4IODtYkiN!AzC1dKscvKI5p_$L{xjTg-~(!{%-5V^ZFa?PXscUcT_u`Inrz z*JY}mb>f|HmAGYr<+ibq>yH{Uc`B}ZZ%(5%ZnBHPm$@(0O zV$%c5>OLi*dzcl8c4KX_AgWIll}cuHqN-UsZL8Ov+e&7f3P9zs>aR`bID1!^NB#yHp(+#;)S*>&>t7ugPV^AX- zQc73Ss^x7kp;X#)%uInen6Eg{tmb8?dfD88T3E{mdmr8buId*MJkurZ1bz-tpzhWmWS%cYSA_wN~A9{R9ENhj^$ulbz%ReQ_5>B|x%4y5%9SgF=R+L$M7_-CWQz=iAb9+pG|PgcM(L5hQXokjIi`-OtMTcof5JT zZeyd6cwL?}mlW1DG{9)y(q^&%Vid8MG>RTk7qCK5g6E*p;-nvsA&60s9$knJWV@%- zK?qk&`KtRWD=q89QEj=!I-^usUmB_c6wW5%z2bP>iS5+Jzx*;&FF90e@4MGLXSV}} zh=-c9n;wqw2S6}XeXEqcq8aXII={}LC&C{dVs@x>j_3-ez%uPoYmpaiH9mwteB&5O zv!evFmWD=~pgD>FRY2T$!cWyDactx8HiUuOg$o;?0R<#p8_{t>1Mzgmuf-F?Kyb2% zSqoh2T%wuA7C#1ER*dEkoo1os47y1Pg0MhPVoO^-5I0R{0T55XI<-Jd9HmNT<|rJ% zG;IJU9vT3hO%~gDc)2^S+g8@9l&8KP3hUp?2 z)uPL1qBs+?&aq;}4by>}J1|ohSk9Y;8Of~Vv0eh~*5bY9@8R(q`Iww1D^9{P8TxUm zJ`BlfsxnxV^D9r#OOP;2rKCV|p%SDb(>ii2AG89(6TY<&+y>}MV=Ll}gW$(tA}9&s zv19N%yW||bdctXU1DpGi; z^}ejV7tD2@s+Zkd?Tb+&fQ|k~AACTKMvzf4M3+X|vfpKgZ9PhC&sWiU^x=p2o|Nq& zMg5*INn3A6v{en(Zv3 zE898zo4@`q_JdmAR;~4F$+RI1v0E0cg=5R8ITA0_ihVe3B*v`s4|NtMI6)} z`csYw$yQFLT&qjGdJTh6aDbl)Hp)GKE!k$r7O6r_KO&bbv;)S^7j6mgGmCZMB+fKo z8+uZr9X?FzPi>>kg-e4tDV64-Hs*BlmTAlc#~g7EqSy*`(Z+)_A0WdVBn=R1hm-gP z3ZT%5XNV)#)g~fPR?5K)X)VxmQWtTkC6dKt(=(V`4$K)%75;qVMx2YcT1y zFPOPXPpe5kI?fcf+{q>qu4MvGr%ZMyeSik8O+j$p?IV`8!Yk_?EwT?f%QoKg|8uDrMlV z>^*l|Z|=_T_5Ee_@?N{W&&$44%Iz{AxBs?E=A#yF;SuHq+pVEq{?L9~9J9}69$vP& zeEvy$oOAqc7oRfsf)jT8+$BeOKel@LBgf7=;n3Z8+IpQ0)?9hx_22C|Sfj9iN_u7gH7sLWu|gZ-3la}s=Pht&6Y+Ys_)c^%4Ef|Do=~9qEs(y<=9n` z@@>1r?c{E?#U`p_o2qqHzPf5mJZe(Aw~Aufsr_7)tP)bi3fy|l8K$M340({yUDk?v z74FeT9K7y(-{rAHm9l+XO4Oc8Y`eEU(7fN?^KDYAYCU+U(57FyORZO#xv5e~3plgs z#_O@@0cX`Qe-y1Ez%2Uz?3G_Yz3et@t=D|b-gO&al~PKtyuyqRY_&0p zO=pF$&1IjIy7ux5eTCV!vfX5$Qm$lHBlj#b3Wm>JbHQ!j`usur?6T|j8y~v=oMR5# z^XgBYciDw!UU|vccisLq&oBSvhqwFwl>4!(m!+m9x6(O?Y*VsVg6fp<&sXl`|FQ(E zOia=xr*$4FMieYkEOfb+GtLPWPv=+pl}wBh;w5ZaJfuo=aSn)B4zPIZR0U-5m*8n> z&PuXniK8{dE8V8XG}-iMrG;G(%GJ$CPU}fSRX}xXjWIWhDPyuC1{vp!07diX5u*ODbj3A-FlIhTl7FRofLIA#nyq`w6sNF4Dk!IE*t z{n$t+ITIx&P#io`Q1n|O3qsA3v1qv-JFg<_^%y&*ABg&z>a zoGnALK}1i6ii6GKu4#&8N2VZP7DmiOs1hO+l`_ONMBVDt_TYmL*x8kaxRQ8m`5%A# zH)5g+?vTzaEue=6;sS12X|N6b!J`gUYx@MAf|rQHAP>3M;ph0z0f+<470gA$)ke%K zoLek&!O)aBPh=beEb<3H@}#f`TC(^%ZAGIYE!^B8R26@+0*hEow5DT-TE_{_MIc)# zF@(**#tp55%@2c47Due$w=}6@jy6Os4Q8P=gr_)|BSe=%Z9|yN0zd{48|x_5zv!l^ zMImd5Dlq&sw8$xAw8xXjL(0nJc9!ipL;9o?yvLleRo2PeKq;#%s6xO&jv~Y(aTx-ZN;9vs|SI|+%&rUiB9`u~=7^Y+Z_f$z{AsVu{ zq9k)u5-OS=df+~lh_X>F%t`U#InL&y3*eMU^Gw7}9|q#|P;1ttP>7q6V)+|4ec2Yg zC(BeiQa%YG4Mtf}m`nc@>9&|r>k<(!-;x0G6Zt$%<76TD&L2e2hsIq`~?U+TpQ;_xo5Hj@rxA- zI!+J+dTVDWXHpC2gBBeFhyiiZ5F%i75>O~7o5J{WLxdeIW7V)N4mh!OT#=;>B|llO z$|HryC?p;TPPoY$$}2dEA)Iu=i4Va%)DGE(nhU(R4gJ^<0p(3?$R>gg1P~{wVy=ag zTEtu)5`^R@L4a9z=z-AI5Dn2u;n4~RfV*mmn84H`-_+V;*FQye1!R@?#EUpgP%R;O<$0QUwQr^ zFZ;5y-21-lFaPX``@J8#dime|{KuDn{0Q~3o3Yi)-uLBZ?0LJaJ#XjrY%Y60_Hp}d zckI5~_P(#97S23+udRH1dCpd=?zZJh2k*YYaR;m)(-B^7Ton<(8h>#&zeg z{7p^gK4f=6tDqHnib|EEtz>nj5)Yyjm0PxZt2|U`Vo=-K_9d%sRRyp1j(6A;R!(}s zwfu9%SFsii0VpGYFN9&KDdms;&RK%TJ3uC&As9a=g%SN7Hh?_ zQCrFKOy#G-R?8pEwxoHYuthY-!w=bCi7g9NEE^@F)Ky=5*SGu3-9ha}J{0_Z8Q{5j z(NET7QFN=X%~Z`xv+|ZR=y*(1&5J=sO$pz)p+H6v3ipts5K+;qpb^#bwz|xbU^O+n zH2A4Oz3fe2-r{xD^|*wpWTcNS6M5ouekV(J8u2r5eMzQ`!1U+md`lpaIg8g;krw1 z`PvPSKX}&<@BEhUFRQQZFS{SxZ$5H48J&Nx{tL@Cg}Rhj?pKyoq9r>mvz3&VFgv0A zJ1$4P>#Q4__Dd!xC*&+xjBFt&2q7XmNwOP+fY+S`d z1LsjnxpC@BXaOYvLSdsEkc}(8luOmE7>G0@svy*+Sw%LkC)w5JFvoD_Qr8(cDMYpu zIDkc_5&19LkgQjIu&ZkbTbwkclRuOgN})N@BRRJs6Zx_I%t}cLnuux%~@eo^x%d7gkyl`EK-OWkR9e6 zs47-c`2Xdfe&;%~%1SF9ap(cog@~K3DZvA$gtr7y{KBIshT9$XtJ+G-FY99Ez8cpV zR_s~Z@mB?_WHDG}K8(nlg?M5^!9clUnV{eLd~1Mg_hwU$&7znTz%P1jCA;vt_nU}I z#QSG%>WX2PnZx)^1Y&}`bsBQp$wgJ6bC7V%OsVPsYepy-@LN6>3mpx3;7pq`CB=;i z8`(6I#Zy>xG2qk=gcKfgC(?W|i$6E~;9%k`c6WrT-?2Bsf{j9)Ca%@(>cIi-VQV^k zgXg>!@L=+Ct07XLd`9&;C#1NK=cN}eq9g(lm$_G(pU<}2|lO{GMvS{J6J zg{))nkofqPlZ{0_)5Z!T;44ZIol<68}-kIsG``mhY98U*4#C z`N8||{rkW9ubk(VPhXLu$Il?#AcY8=d%(N^Js_i$vjq?G$0%k}^gs}K>oDk~(6&HU zt@G4D*h*>P0DbBT_GTToh+PUPccfuRGasUuJ|buG1d^-ROi&EbQTkb<9b3=fdrP@$ zR9NtvxKZ8$@xg2sO@s;E=9_P{bqwe-U3f8LGn6MS83#~?+lJr(5_4)*NuXdl*G$|Y z$}vB%Xe|dT?yxmPDkUgF(Rr7v3^nl)H{GmbScE?xa%PbhCrBs6>>Q%pbC5I5Vfu+s z-eI7HqvX#R(xn7*ZfH5LYZf5SG1HmDT+L5i#2APqg)^5@$PK<_3tkv?5&1(EwnVhF z4LvDu2s1FM3-Hi0#51>e%Q16tkZ1+RkrRbxU4hC6OHbigvf+G=Wje3&fkH!W<0(*i zHEA6)h5(K&Z?(EjGh#;qwZx+*MJI@ddtFb68=W+i2+koj6w*mt*Qm*+jdgTgN{pH^ z4G^P_9Pu$q#N~)Z2>$%n;+Zr(i5m)Nn*^7Ow*>z|{O{1ZhV)O|CB=V)xBH%yU1qnL zJo}XA)TZvl_Lgj~{DS<4U%&S6fBA~4*@Mi#e0A!RQtd21{n))PJoRJ6^6M`>>NQ{9 zj17EM>gUfs^y`;@`s!2nJ@?4>9{i+>h{AwP2G>ZVAi^O&f3uX zu@9Ix^OQq(J@v3zCmy)tRLQ(xi^Jw^w%3kp?Xua5Gv7PI7W0t{H?hC0Ot!h~{n%$7 zv(vGAZ*uZMGq1VekWZa==!u8!aP|p%df(SttIqIxuVt5o$pT6{l&s~1w4OhB_ z7<}P|YrpvU>y*G#uh+ir>V@-mmjSN0>@u@<+VR9=Kjf!iIe-r<3BD`^2`|$}8-ind@#a~frf6s z^;=ZAZ+y9>XV`<@ew&$Ub014puyb2=@B7ZOPzsgIJhzRm^!6UJX5`jr1+@zItTRuM zDo=0qUZh@D$-0|b2`nm9euc1K5>)-d z$N?1J%yQ2;AN=@W=bc)=AsAJj+j6 z_?DZ$pjdW4_C@EOZg*Jsx+^YHM0b0(P3OMFY$v++V_$aRsi&QI@YSC@=S$ar@~TVE z`RW(026ukvtGyrF!^O>W;veLxm+TLGldxV zQ^sD7O#=$e5T2(8o(3&uIju5NHZIXsKbpQ4vFTSnSf1B-%gG@J+?qrdp`SHYkE^0i<6`sV=t%BwHD@Z!_prI&qi$!ns$GFoXlg}?Ut z)T)tXw4+Ej6TEeTaEyBljZvHonNfg0B!6^sNiZQaJ1^)FA{0DLhlnmiC=8hbiuqz5 z`H8$;!cclC6M*QdQ{={KC|Pkin0u}5BliKR9%1g~*a~EKV!I_<>&azoisfJZ`VGt~ z>mGT=sh~#hb5x_^MptE%o!Y9Nu3Y%ZVNO~#b98_?KKU;!X4F-5gyMI<`Kx^v&h=l+ zG2j^`N3WYA9Kam_jyJ#907sO=W~xM*-sv_w)rzi^Ou}Qn(jl!ll*<`{Q{q^1_(7Z< zG;@Pvt)PR1hk;`Zv@VtmgX1TplQaetv*09WOo53~bP={5yyBi80a&*Rh$=_%=NnG> zs-x@WpFQ{LYcIa>i&q>@9cgtPd$0f8SG}yxtAFut|NNJ~dfidykZ=OB*so!J`u1Ob zsTni;^2|Mf<-A`tMb*;{jvV|R(Gq;eM#qVVDJ%-S$wKfrVV&S2<2M9)h&$Q+{^Npx z=zmFIC#Kys2GS(1%pBxh)#$x%>If<#3Tl`J%& z2@)hK2$&EVa%`IJ1{#_Mx|@v5=!`S7GrK!`X2YD>2|K$pvomM+oc+A-SD*baVb7^k zr=F@?w{G1^_g7WV^C*H2r8ei#jN;s*V@KKbLZG??R+a#x!bzrD$cs7s^A$vaGB#3> z5iboR!<WQf`1*h`BA#gtlwqzQtxO)- z6_IYm=P*~gg2dBFU4?i86T>uC6)-_;)SFcC^rD=5bSy>&A?t-&5Gbzd;HlOb-YTms zoQD$eN6ZPJM4;AuG_t26oeC)6GX6sc>Z^_c5VdZApwu&qAm=U;WC@>|CgGBgpT||L|RNOEZkfKl;rdh_V0n z+?-kFjelnd{|<&XNGk=mK~908T*MlswU37NK#yUTltYT)K zA+L% zCFM|>!Q?`Z6(`_Pq_PC$#Q`5-MN5@M7(+BQqDa-W0g@yYq={uzoYZQ8o0q86_n{Nn(;*USXmlD5Dsz>QokAOsUBrtY~4%F`{wi zXg+Qw$yL)JrYqG%j|{yH2dM;8D4Me8dlacJ=~22Izp*LCoWti#9pW4K!g`}i*Z#l! zn_v6zAQQzTHhO0nh~~R}?Mq*yJJ{n$-}{1^-+ucQ?ya|8e)G+j-hBOq*A}`QHEY33 zGy4nk{O6x|cKYwldFtK;&p-C++$ZMEe(;6a_x#bj&++Haeejtl?|O0e&#$=f#GSTW z_29iXIbe^C587j+!zOQX=$;$xzw^2W?)u3iCQmwSuMLmdchWKYZ~D0dHv8fsTYcf6 zE%x7e?d>Nmy~TPT*>#H*r|h)WfxE7I#N>@Z(xbIu8S-*VYWmz;Uj z^;1v&>aqJCw)d86t-L6_-2bE&@&D!Dx{oZf=tp2N$PQ9wBCfOMssJ6-2H80hwJ2DFo_>T=i`ZxGkmDe2Ca=?*Q5w2L+dfDj;ZNz9;4&+v&MeA$jQvm~|W6 z!8areK|3OMgWaO(fE$#yWWdg@l>391+o7`4l3WB(kfUdMKJM6~X_XVhIi3c(kvW}L zIzSC{6HGy5ATzw|>=;JA@Y`pA%-m0IzY$WNcG1}(XxFd>rk?i=F%F}DcnfGe_1v>g z@QLipzVr3(U44Q3*#5ly^B>*h``C}&{}cDIpPBxU%h>ato$iR)y-L$^P^E<|GnKt3 zp7Fp25MzmK0Yzgqhmso}rRHR85tPwL^fB5_ije49rm6lzVZJPNRy%;hp3x8sx@!qb; z|Ni%X?VopLTz9Rt$i~2CFncW;8CBD#W`R9289rXa31^u#f-4<+phvFzzb+Re}OMz zjc`o>2U&PoS#jnID-=(9NGgK`bCMIg)Jhq$h%YQ7g2>QQLNNlInD;oP$cSF8wQ0Rq zT^Iu}9y^Lf#203$FdGEKKs%jpf|g>~3C}(^vxDP~m>IF*TtJwi+u^d0eIc9UEo1k+ z$LP4PgmoUx2dE{%%aF3{eD0TKfZzRhiKsNJz4RObqkvjOYDAB8MPn+$X=44vt47N# zxtI>tFCJ7AjfsmY%>4e7xYPDq)6b~4jPED2f>buVtX}Htu#B&Qu&Ps9y9DClQv*k# z#NZnyaJNErup*nX_#PXB`iWA6#TXQtPgr(+9xI*JBPWZbQKO9aCETNSsnzA%wkAA6 zaNb~Lqh^M&#DqsUdc6I@*VA;ZmtKCJ^Z2V@zw-ybdRq^yr`fsznZ*!;-V|gGhs6vr zFJ$oc`e+w^EEc&goWKo>P zov8NaJra_wA(Ni<1WZuMqIM$1=sbG90iqOgYXaJKu&dEo$l3XNf=*%x1THd<`D%m@ zp7`hn#1IhE;8HXv#v_pkBdj9TMVJ8M)wk}gIwXOVX`7*9GNgZ78EhEJvFD(55UV&J zPt37Wo$5jikMYE|DyIr%2~>13Wz?M*|EeiaSbaeNyI<3G^h5v2;(MN@@QqaT_U zAZ6+bEF(Y}%8-{6<}0PLgu#<07HpeLM9K&(ziZ7kSX9cn>XB1WB@V^OTM|FEl8|xois3NiA`pnlAZPJO7<~-I zP>VION6Kj&r6rMil%C!MWjq?N{3cI>FaKJ9>?YHCF2k*YYG5c@+`2#mU_TVkSb&-R}XBmSh%?r`eoc0TEltu8!e%8gS`xcr;PT==ykjy-7m9XDNl zxy453%O8lB!9&iUMuAF5FbD}I1Bp&-!DOegd;qgk&RcG};-(uezv22z0cYQHzVh-5 zF2Cgb%chRu zOkHY;MXgi7K1wehLZI*_bgvdnV#mszGD~HVf}HB>3|+k}d5|?Su+$6>kXrQsZD*{i zv-ci5%i>---x7cls&!0kV}Wr?V_>zjU?M%GXsN@Q5XT0GU^1%SdzSn zq1?nva#aXK2t+RFB z=uH?kHF>J3Tpls0Q53-;=4&zpDM#tDE4N0J>M_pplB()w63|A?&Ggd$eJ9Wz3}kU2 z12Swyy7*zduwZRy&}3&%$OsP$^W81clhuNWn}j0Su_DPYV6rm4+_y2T2#FO?V=MzK zV2jDnb{4~!ZgmD9Fcj(R*x#mV>#9J0G!BJH?)?!5#O5NGu`%T6hnC2oL|8+B{4`)J z8|gTsA-sTg^uo1i!uJdD@$tXK)E*c1q{oOEcQ~=sJf_*0OMaxGb5Qy z%psPKi^psvsjMtRgPwIfTQ^GotN-*D-d6CPG&6<{mTOX#n)1nCyZ|xy2qXy@#bP`L z-*9H>Ij3{c%Ui>bK6oEQ%r1qRo$r7o#G`YI2ZTZ@_L$CAU$Q@=%guXW=s%s~TgpH2>`Hz23;a-lg(5tVm zVvrIWF@k^jr~gL7HYm{0vs{|Sw4+ic0Wp?xGfeT4E=&TZ4jm5|)V;L` z{z~^beIHXLn>WZT97>eM>|`CW;$y4#Y61mO8kb_}lT$m7>A%Tbj3!9!BoRg+FD3@B zB?)JJ1F;roB)eD4K%!)i6s}Pk0{TXe+1n#CevE$`zt}2W`-mixf;a&k6?5{`+{)z$ zz!8aTQU&;|0{YjT%$817>+*>zpe?GPD(g$Sx<^h}j5>ywJvJ6383>Z-L01fFdF1p-t6DPy6-6l{4q*owQ<@}t zb2cXzAVab8;)XzcVA(#K;SU|d%O(N7u*o6U9ThXo=(zcJ7+EQ~0E_tdl$f0Ktl}Do zs;yA;MhPYjU5-yP6h*fhlqP9OT~xCdFSS!R#Xw855vohH@MH8WG13z2vRfe`R_24 zXLYcP3!_1H@UnOy^wyiNgqIyIUvu?$`~}Hxf-k-J+>0;#HcV}g@uusq0dgHHue;_N z>#XsK3(q~rBRG8Zw7xyNxNl!@Q1j{^Ur}*h+JcL%vwrm8kq=y-e)5THud#+NdpK?F zhPKb4_#P%O?y|R|XV81vB^UXcggZ#ECy46SG@EwYt+(EDw_VKDASa|elJ=(S;7$LO z!F;);m-f=MgJV1Pl2lESs=>=~tN&^<5DnX&N2*&s73&gy1bHbwKz!-Ml^fHZ;( zCG)e#mNsi&D|)S-6aWf`~;T<0J`ZX(*Q?KeNP2N^Fk&q*DEX zSfR-vFJhDmP0y)XMZg?M9tr6Y!)k#EN+r?&q$!?|xK($uDXRvmI4 zI*oGBY;M=ee5DWu20>$dx%J`y@``Ya(u);}#=n<4-1%H#*(F)U&VLPa#ewJEt}&l2_5@Nizm(Ux~ycKat!Hb<(}@ z2J5+k!f0pzYY|E&HkH=BiB~mgL(#N~tx8d;B(ByJgN!m|mn5%vVOjVZR4z_f(#VbP zV>{OajTek@F&|n9)Cj;bFTe5vP{S9A8@93&Llvk(uiz@N($V0Yz~TietF9=l--`5D zd_0Mbt*|7yJXR5pA{C?x^%=z}x*pC7Vi&OiSVTEPVsPR)kCGTmj3`hS=5^u&_D;mh z%v~|FWP4cR*YAWcFt88XV^go#mS6K;+bqZT4-FZEKR1d~x%dlA6lrZ=jr#oh~s zlp*>e1&F~UW6rVA1e`nTPIa*NoG*{M*4uQ>o^yOZ2Wl4Mwzg?dejG|UAtw&%XvSkr z(LDY*R2KnN<78Bvf!uWDQHgv-t0q6P=&QJbQcg6+yS}7r)#25P8vun0dQ|c13pl8? z$0h^;1R^UYW-bU_l&SHV<&nvBTv~nY)SY zu_I<=;zyUae}_;EieOaesj>!Np+r@prlEftplw}|PCRndB8d~=G>~3uuXt#8a^es} zbx~Xe1=GZf39eQVkD!xy#7ak2On53$LtXbA%DDc6<(HY>UK%8)5{CkxMONg>$(o zLMe$ia4XfLBqa_6SQSYs@#W&zqw28=5|aQj#;Qm0oJcObEIxJNz~Mh|!#{Z{(Q=^r z!ZDXo0L>tt#~kJ&>s0lthJ16BJ|@5k)B~mJjj%AJ3?TD?<=}G{v->`FN6aOSAoKWe z`M3TXbKiLF`PUcN^nT{0IZuGh4wo0a@Z=jWJ@wWrPxIZ%es;!BpLyyh9MlXi&wBih z(~sN7*RT)V^Hclovi`n1t-IgO>%q%=Z~KWUJFRp0-k)+A`{*g329_aW;ea!MyxW#5 z0n6)ud=aP2E@pEsW;EJ{Jup8IOGO&E@cbz+r?qg58)K8JlMg#F(ap}c??KWF% zzT{#bU1QZ%Zt-I7Rny>H?=@SY)?a7MpM!k2+~{+W9{p#*;j-gq(AY;T0pZh5I>EFL z#GZV@mn^|9X?Fn|UiN~syceB+P6yK=Yp*;L6)P`a+u(I&+F7j~PUzBl>MnFJ-*w0B zw%OdPVcsTIg|KSK6mWLp476Gh!BRk*LQMYR?I$~8cIJ-1R9nOVHG|eMu}B4pQMxm0 zD=)Urm8{WjakId^dJLQ6#_DDvvz)D=99H(96=8AdM4Mu1*!lu2cf@Q5gW7FhI4fuQ zBYxob?ndC0S?Q}RxA;cutO^^~PI`hx&xEV$UqMbNP5_Ut|DKpf3^L5{W zmyiGa{-+#&=#`gzbL#nD_5bCc{P243FHe8uZvS8Y#gA`&@SeN;5bdo6FFf_gy%tI~ zo&}4uI$O>-F^wAIs1?wo!Ggw2K5;n{w&Yn-q*@1YW7%>79z|O9-tX{H1gAt~I3xgH zamwmZc7xP_fK&r#rHDZ+QUxvF>`C?#V}?P(ssxjR zmPD#_SxA_#I@T|~)TC0Dq#^}Ga*E@Nqyc&HQ5;i;gbXMA6-{jP0(@bj<}xaVM;wHe zn83=dE=)v9D3T1+r9_XyxG~IB3UlkJNbp~+$s>%L06qj0=F392Vrnu1$W9NLPiSgK z+-gnPIT=wZg`q{~oJf55BqR^TDsHHz?6Po}R7Y%66k?XXCNN;zr-GN60W{+z#kg<= zX9RU^mC6fc#2QmIEO<;x|KPc3Re z1GI`>q|};#FUFCvLp|UHGzL$?DI$dzjIU~sFw4B>o_}HX3v-_lh_k%Jki8b<6F?+4 zQKXt!q+E2xC#?Xf5}Q0Zxo}XyBrNQ4aH?RmGQu5ZsXGm%hwyTzu0Sz6HjG?2JDJZO zX7jLopjzk1L14G6yJ9W!kN@P?!cWb3{P8Cq_Ojs+j~J|WvOMwJSxhh)`sO-}2APEk zR(;h`O;v=PoIylODa7zMUUYj35ll@8Vx zjs9jI#v`QcO1AgtJw7*Qrp{ImwBk?{JupkC$k;T33`t9^T`8N}0GfHgH8WdM#tz5; zy4tNW`V4U0z7A<9k(n9~KV*N?h&38Fqtz6ri29OusT%uqCD;G{*MHPf(6L(|_T-hj z)#j7H;XQWULAmr`2OCthOKD@g64?ee%ng6V(HKB#bUR|^w4<^#w;rxUiftean)gW6 z9U4+K;BV5_KnlXmL`Mk-lt?NZ@F!%zQ0s$!`h#vStdlUI8Nm99MiFCB=o`#t-1JEe zXYy$%>8=V@5auKlX`qo4*@*x6wl;alsr5;MQW8oPw+}!bRig?zu~-C2;jjvH-o9%bjtt3`@0ESD^tu}n&SNI`u=yZnZEgMtR4w39$H;o=C& z!p_un^-Kijkdox0BE1)L+M`3AHZY>=N~LWol}kSt!vVs%uA10n^9Un5W#(Yy9-TNt z(4(X0F-*f#Ud)4ve3(ViQYCQ$VH^t7)@hFdwF3R8D@i)Mtt4m7Xef&IWF$X1>llF` zO+OSTP(e!X@UemU{}lSW1IhtRVH<}eFI+}pk$A?oN98IsF~}oHjK_2+Mqa+g;=^>o zz@Q&u<)Y_OOZ*c?n7VAlDJU|2;)$vBgcM)VDN|B!SSwmg+yp$vO|B@bNK9~05l$dt ze1_~X;SX6P#bjbqHHwuwMz40lMbgZB_{V1=s}xdK5hfm&iiR5XNJ25mSyFj%^H@?v zM^7FB0*v%O-mKH#a*cXjP6sQ@N5df)$IHU(G&5K={_pF|;BzO=FtWFpdyyHq4k`Bv z^V@H}=!p5vSLeLpi(aqHowxoKj+chE6+XRWclgG?|km*pS||-qc6<- znfI4Z`_g28Ti$>7jhrvVL(2JHWEMDJmvvmj-e$uk zHeGuW7qd5CcTs1}du;u2=gaP6f9p$oobiQSt~vj>Z=Z7DStlN}^@c0{f9_+0V_;gg zsMl;^XU=D_fi=dYFSxpd?6}9tj%5gn933lPd4W%Ud4>7r8?N--=BuV%1TRZvIyy&o z?CTo#X!T!ro$udtBdF_q89-ig@x^`k%Oz@mcKF5_rz3E7eCb6OSkq@_ z&+vUK2hh;5NXXjPW1S}ZK3HE|6yr+v$}6mJ)L{o(pdn~CoUyWil8tprcnbljOL(gu z$PA5IY9*Q5p;u%UO*Ik&RUj0}hnWJneXp*~h+Iy@r&14cnP=c-d`jSF(Ks?1t~2 z|AQMZ`NC0?zk2df*IjwuMdzLlDgW%w8*ab(%9&5xJLl<#@B7(p5B=gt4wpHB)-S6h zXSp1fFps8d1H>%Nx44_15r`DS&j1jXWJy6I#sr>4E#R^2aS)b^LmxK`(c}_<->B@d zvMPOGB~nHXjYvzXvJC3%4P{AcT}}Y>abPzx`P7$VN?G)CtOY$mnB;g?BWrgy6>f!P zm$Ah?`KzpzU6L^6aLV#XsuY`={nLwLmdu zvy2lJ2QZyUk=#nk#ulS?1VAj1{2tjh@`_Av{ANf5gyB>lnSRbLQiPZUQ+)!X7a)%+ zsJ{7=ImgbYWLp6cA3jtUPvVJ}3q2=eIvo0jBs7|gA?Fykk)r<_Xd<1%8ZWGUF+PJ} zGm6CvoEr6ZkN{Y>9~DO-1`&G|+4}3Q>pOc4T}`YkiY1%-kj59D#`S?yrXcHTih!!Q z@Clwfx~d?P29=Uu-N)rFXA91@n+-0AMq98_D8UOCQjN{dmB!&E4BL+_wwOrohR`z( zz&JHZH4jL#!vXBwRRiI~%_7u$_@JN86@6~Fuqc2je zaX?&K*Pk(uN$orPnWtc3AcdPZKG$==2R<(tgJof`W$ug5&UtdyOi)kJx40TH5W`a*x17y z!;EF?v3Oy&8MDWEF$fI&&UP;L%nSiI7jSkwS}?&x0~)w*`95oyERV(6)q7&yoYsmsGFEkC-w(ThPLKn)B-rqC%Aj zACXWeQHnuIR6gfXLr4vYNthLRRw)sPj*ws_$+uC$}uV_nZorhk8+t| z3{UI0qAk0J9JsI5pG9PNTGL%ELuR6|<3R(|g}JSGkz^o`smVP7_??i-GA)qMnhv=e zhV*apw*gMKv`VGvzb5c#y3@)WI4mr+QA`-FS&sfG1dXB5VXG6*4n6r!`AvmX%?vgW zK*bJH4FNF*j$uW3%bcz?8bPnj#aSuZnt1fk5ECCUQ4<3VIX$W}4*IaVm|#u3j4S%r zD=0)f!SJlxS5uGpqofCU5f}*gaI#2JDaDyFw1pTo9moIW5`yi%%NB}zgwKqpxS*NB#sgfhP0VS<2@gq19*lrK`M zycp8h2$YSBCPUM*N06B>X`&biDs+hSD4f#bSzS<8K}^J_TQL|alrS=iG$Vn`sYn(M zH;*FalDC2;vXXI7CIU?O2*h(S5fD?+d=8(1Rg~pI?=eaQWhW#Fg~ccNz#%4a7|NCEuf#goZo-*#n)b*<$GVwmwo9A zQhxc_hn+9K{p$4RXZ`$@7as5;^W0f?zcv3c=gYfp^9f+t`SO9gZ|HkpZemZ~ZjC*+ zS&NZC-K16Ev=2L9cG?0z!pnA6nQh+QWhh!EO!Za`NdP9i*7-93sw>Xt zpyr!zoaXmN{uF%m6&GK0!M9w(aY_r-dX;I1&unA)IAGs>etgFdJ6}Ha%f~~$pFR8# zN6Fx@Z+W$pL(ul0O=|+anC&}Y4wt?13>QP$POI^8#O&XNu5Du_`{tW$?1700B+T@9Ig|}hGkWRD2@zeR@7E6xtKOYbD7?a>>`H(e*yy4+C|U}d%S0abY!2FrhJkxy^1=FG?M2b0hG%8A}w zcAD%1UyhsoKniO1g0mP1di3+<%P+j{%5!eJ>C$5k-_tA1H(z)0cg{J@%gcA)b>ok2 zyY|6f{BZiiKj)xt-}~|k^Yn**VPUd_nU+n$)+}M`CTEuBTZKxJ%61awj2hzzu^2;4 z7$x5*<4|&iMH*{pIHRwOBDsm-h=(kd6%)RKETB>=f&ipSFp>B(GhjCsg+Yx*mN-Zy zp}Y|b1SGB97J7@gWypHXnq>w^;;i5q!1O=^Qk9;Kq>hsKlCYABm_2}%Eq7Ld#T~tR zDNZV4k4z@UQkyNzEk($SNd!^iqZJTy0v@F*MQTNhCn~XJ2Z|Np{6f!Rt+^55>Zaae zJQ~{MVRxxDvyfmEqBxRNN5EDj&e(Ek2?3{?q*4S`s7or6ONm33YkeBFOhcnYQlh;e z*FNc=kod@=MU+VSwUpvGOj4I5q~=1JtP~JOiMBY_pk#4XdViRx$IQX5=+fA zi}_r5xpQZiv^~PnkTM52E1CfKOq2&_+z24G#!Vjlbsj$Aq4kNUGmr|gBo492>Vi_z z5SB#S1jg2|(i>UhdYW8{BS{7@$Q*=Z3=hzlZN|hc9$7XCJJ!)KU<)k!s23~@2t&<| zjKSpTPdzqs)^uSeu2-9(W7n`tg@|+DHirzc!)Qm+XfP?h(W(}jn@>o(VuW~9M`5*A zouHFydNdj8LVB$I6Bm}8kfKMdnyPPbNrn6Lw-L|ri+D_Z&be6jCsRilboF7()mC0% zy|q5E*KRv+@##MQhbymX`2(! zKpI6u*zp6Kp)+E%YhkWoxiP@R_`1k3M;->b+ZgvHPll_cwq^MGl`dzmrtO-pH`u*- z^sxsXf8rrW%u+e+t~eDpI=tCrVKofZ#pV?&#qe>^43Hjq*nw0{lP**nRx~MK7@70$U@+2u>ZRLMO~Xx7d`TXyV&P>3 zB#e?sIt*zMXepDJQ(o8&`r~T?k6v#U)+H!h{}&d?SOOSz9-T_Yh85vkV$uM%QcaS; zuYgn?)wtFK`i4|~(>OM?%^8xM!H815#LK8-V2e^lPC%bgDfC?tF)JdFL>r?aJf7CX zs1D62QKYg|nKp*LC%b|`fXx27nF@xN*^ufe+(-x0bW6b2xB#4k%$*}6;EF*L3Z||B z^60}<2gN8NY!#wu!vUrJhx936;F~A_VJFCLUw6vPH&+zdOd=+{jN#xQnw)}^4(SAx z2ta;<5tx=V7Dp%4+qkoE+dy^;k#p3-E@dUsJS8VE4qQC>Ls${Qs=!w?{HVkVjZBit zR$p^aP9&WV;t3!goJS5o!&63K#N-btBNH>R6p!jFMiS1*%oP+ayGVI!-+_NIu}yMh z1BW4A(WT0WKs=X7l8TgT$T$=B7KuPil=x39H0JMTI8;qQ_JOlU z4s&{rXOc&Qj2lE6h|?8?+hj;Q^ofc_3^8;A{=|X?o&rjYn=tx{D5f)lCZ%R11JLvv zKrY^N(W9gemtDXP8Uw|`fBc7r?Qbt>!^=L4&6VV{*dTLX$^PZrFO4Ac>wen){JU?= z^~tY=bEm&GfA-t+XZq6DOV2z6HUIMU8R6y8t?Vc7`r~(>J>$eF+i$iqusnIE^?dKk z`7*rhN_L<8f{}gfi|dHlqa)@c_xY4lW)5)PbDLEhG;g-f=%Cr>zxLf~W$yD+Hu>7I zdtUbK&!7JJ$&q zeeCP|w_J7S?Kj+Z>$ULmh3B1RC6WSx#MqnfN!2Kpa{p4c+@_u{oVPUnrLeox-L1Ul4KI>~=L1wjeY|S~U_E8bYTI5Eb zTn8CeKC$J|8doh?q!6p^Uy)p|()8sntEUNBj5A_80Gu;%tDz%lwS!4V_sK82NOoJ< zENT_EfMR0#vxswah%WE)Yg8+t)ZV>=BD063XOX{39PBjLDE4zU5|{(ElpSE#Zh67WU6&AHnuN zDPf<;_6RTc$uHNiMfQiOYo>i0UOxJ;J-+?*FWq+2MigR{^3sOXI4e$A_Q8okMnCwo9M}xl%3IyjB%?zAv>U@Q;aGr~ z2dXnTeDEZRqB#MN6{l#`;VW8;@Ciu+`V3&G!>}aTdqKqxN$t|;gb<%;5I3@s%EAgR zsUs)gQ6z3drLa|B)j>}%dLVfpu0c;E^%6-E^&*l+q(GZI0He~00Zxi!pknS)G_`02 zJY`XeNAhEW8*SsDAgs{%kOIlqTzz#Nz8Q&u>f$AX|0j3dh1fQTohy|^Fs57?#>9}G zm1r>9_XDo0zAY?Sv_1lR6kWDlewk&IMMC4w>yR$eNF^st8nZb2z%^fek%@G!=Msn* zJXKAA4J=X%5*Pq+16(BMSlLKd>(fs;0j$<=iuOo{y)EL9i~NquX@1(JmwJKP472eD z8_)?YX(l~~D(Y-NlP1VgLZDH&L3`?jKLU~D*BrGAAC1;5fQhlG>Cq$OLuzKTu43Zg zAP*3RRzTzV3w+FK^xmkj>#}`tNf>N#@W%np*s2cx6|F2x#E2(qa!OK`M@~)ENPPV^ znQ4{Sa@ENRFNf4Umx=@oaKh`HHIwR-U#&H?x|jyFwE!Td(}G&8Uo}NWW9}pf@Qq?Omt5>0u^j^h zN4gv+q`ui&>i(5ezTC@7D=$Af1Lam-X$8wodrcNBtS~1Pc}xiUhFRCBX<%wo#ie05bHnZA9o!n$nD6??Oj{JJ@ez0990;73X)u2LBnSoovN-H*CZ8)^;Uqla1E@ ztN-{PG?pzF09jwEQ`xI|_PM7$ir4qxXz{K$_&&Do=aE$8G#JelhLuWzqM$3C*tqs6 zo+K7fw?|aOt2C1~+DNUf(PjhtX};E_BKi`|+W})Im{_doK1l9J)b0TeMx{F{E{U1G zY-@jVopnH9aM;@IGKm&6-Yw8f2&+FEV5udRFq03qC40%tU=lX!!Q^Hk;VzAkfznI` z&c2C3+NKUtiRV(XYHA&kYEc9Ah(HtMY7wbu^bmsuPSgZ|TSxqsE_V!qjg1SPL{~uu zJc?HrGRSMjGceU%f6x!-6q-q+ggMcm}9v}T# zKOteGRjy(XSd8f06ojXEYz5>(AQda62f&qX4nd<70rD6%N M<4k=PzB(G+IzLAb z!?-XZpw!=}#b(0H>5tpkNmuBl4(H3lCJJZ;xX^E55P`-N3<6Xt1s{H@Z@0D|^GY>y_B(aG}oJ~sZGVS*3lO5_wYIQ251NTuOMcz=AF zOgWUyH*f1abZ9vNT1bfA`95*iH3| zMH^bCy*GjM^FWkc@#OZtcfy2fGsCJ8I*-yl1JHNNQ|LDD^843 zA{#O$VHxp}caU5p`Y027kZK|m9FRE^r!E26i-{*+E&?cBt_0wVsRi{tVPvINP-G&9 zIjtVm0%7LYwM;?HR{}a4XFBTBUeNLA-Q?F@c=LzQg}?c=wd4QdWq)4g;APZ>~Q&)uRiztuRRMdzc}jw|C3rc@8KDb-7#n8-H+UN^HUH1;NSgb-ZxG; zXxmS(=!eUD?J)Y-mn+%+B<1C0sQKtAn?cIH_XQ&#vWNI}y~W%?b2yss_flS7?hjKN zuDZzNEmqun^A%1wbgPq(+;QqzM}KzTt#;mgjg^=BP`u0(1Qp>#uijX%;l$o}GUvOs z$TW3I($T85V|4m?^Od*%;2ORo=BuV%=oRMcuesbWrSJhBo&Wm4*M{q@>-hMp%P+h2 z#v5+_-nFh+yKe1L)|Hn{bsyU{IN)rhv!2?5c1C>PJwLOa`cafy)}}Q_$zZjkYQ#Qz ztxCM9ZtKInw4ZUup|Es>er4z)pJ>!~e*&Y&G+TTh{2928w~ zRz^0t``aXO%#FWQ*xQaC;erm1E%Z<@J_0n1g9V*PR&A>-O1smhN9Ecu2kqBdWm$OH zv9fbypcrI!vg{@{-wo~CZoHabIQV?U)HA1D{B4A#YJ(vd;K}APHLIU8!Ho!2RBq5J@G&E+#%-+tLa>nXz11Jag zG0gN6BNqw-CxzVxWG&m10qY#FDu@_r!BtRJlj-9r5U^nf)pSjpCrzn$HZZIMPy1svFEIoaHsv@K?0H$1c@aDgt`5X*=yT+-N2} z6HhLqvLWXDh*UUJHKW&vX3esPRi9-mM!A?fr1R|p+>BPcRm4^b@m%dNra)TNMB_Gy zLv0mNdcBke1jy!d1mX$C=X5jla>;8fD^W(R;3S%&)Vg7!u?S$^Ag!Xc0&S?iTLEM< z12~g1@Udb})0m``LqZ%Vgplg4xSU4>l&01KbRu5$Nr;R*2pS7@{)Pz}@lrXdbSPdH z*=Nn3J~}lX|6v(y3Cn=YAsX>83xa3>8I7(u4o@W}jP|YQB85qqkXl>-SyIU^fIz@P zW1?d`6IEnPsE!juV7fx4dz4+f8;{KCI&JOFFbfp(nYD}uzA&RYgBF5dNHcN8um=LX z@UIKi-cxoo>^^oDFSFKZkw>Y>pkpUTogYa;115_UhMn2T9X*Sb#o07&i1kB#hVYc5 z;=+Uwv0H6y?c24L=1IcA@X(MpVSt^F3x#Gp=8A5D+)ik2m znHhUt5Z-drN%pFa`@%6csI8v9qUp0C{T0!6E^e@)egzonlZw^>l1ER|jXBdx`hUN# zE$UHxZEB2nJB4DQFqW$Mg(U2J7sFOraXGibAZ07H?QW~TNY)wxxB3C(&Xjet_Gk1f zftjM{^0iV~U=6sJa|isy*WS(s3wClZE)!A+!T zT)6{|5HOq#Y8AA1V?^kRhJy(KeBc`?ASVYJt3HHFKL}8Apspm@QS#eQgLeirr>_DO z##-2CfEWV61rD$|RhtKFteG543th(UK$*|Em~A6&a2{pHHs-=g^k~)+!;xIW1(xx5 zmSjsxQ3gJJ(ijb}9O%ao=8Q^;;_5Rx57|D@DZ+FiNukM5S#(GM@$bP+6hbX%NC;se zC_8$rD6RQNyzG+jtVODYj3N<-&X94G$;HZyF>F(yR2;U-k|mLn3UPu>lpjXslo+>| zzsHw^TVz0o+>;li>M-T5a0yF zbNJH_0+bO{Bab4H4FU*w6j{c?9tSen)4YmQZW@TgAW{NHkN|NW80u7ve?z!wr}1va z(6v6$!NpLp17%mT9WjHlY93+F!Y2XD+c`3rmQ_L((&?+aw!Z|C)#FCVhU zh7OmZW_a0AvX6bi%Unp=m24o|;j(zCy~zCOwHEP5=IuVU)FvNaWZQKYJ#w!N&ivBe zUq61IlRvwM^W~2({h@dnUiOrK)!Daer8wAOkP%*{JsA~2 zu)h7pkPa-k5=Iycu}hmc?7qX6@ba-o9Rw6RTy}^IICm4#9q?<;`V>%Rc$#ms0*G_2ff$J6ZN8DTm9?&wR|XWm;~@W1+QlTGywOpHptuvGDuYylY~7B7sOfK=rsNo0wE zp-B9*I?7lPl5#OeCLX1ZVM0hq27~3Exo$nO7F&a?&emlaF|?+0Y(o7?b}<%u8P%5! zf{a5T7snnzfKLJOa3wsh2%;uo841Af!!vV&d4L-Nj@pI!m|(6rPQargtc=7H5Gj`> zQcLMDUbxe@A+^<48NGl2E$*`8wk}@ka#?*aEKo@vlF%L|N+BU>b8NYx6hf*9moNxa zIU5ECS~4`u55m;gO05qAgNBLIQt(^{wszB@l^r)~*U%Uu))wT)Q|TO4U`QLZB$8|* znXkwk&5&RJhfjCfetQ}aPjY(GWY`*pwxOI9yCQ;f+Zs54+s>8THX(`E8r`DQf7DJ# z9l!th+b?n1Gts8#Az8KaNB{uVWpX3erH&4raaMhKRo28vBshKbRaRMJwbjfXY#5zf zy8!8`Pb37kxonllB^6tBl%3fOO=~GLEy!#$L45te&}OpYPXN_4BADeA=utBffW}n3 zcEPrGp(ydf6kFfcxb>VUsa+E^xDe=F<;6;%EFuxl8-Ms3S^;E$6levU@z{xThs%(Q z`>`N1fP;gAWXyRa2~j;_h%-rqMJBmQk(Q5=v`Hc?pxxD6~HM9USF>+BC%MB=Sy2KA7@L76nV+J!**>M{X!M@F$0BIpyzT@DaF$m0#<%?(f z_D1tS#tdfevV@t$A_u%2;$|(w-N5n_Pd?JcYy}3RRF;snk>=opRY$y$s}|(v)btPV|>!rGfc1J8#$53l)fjt?N!pvnB4*Q2)#Q0aozxeY%W#oH7$qUQgY4Xoy zCfR@ZxBup^|MOom#tfVv-EoImPRFZXk%0oma|%)lRTIx}&}aA`=p@y?#L#?D5}sQ%P=mLPV|4V8eCt1WE9qNtWg>U;sAJe1w-dtX=aGtYfc$xE# zfPU!OL;ExO4^1l0`tJ5Mw&1E=FLRv%XvhS)Bl!glZ&Fm9^YB~V8+;*D1D z)&@8}7GhW5X`43agsKf#@YZZ*`)h}7x2>_{U4~h)Av)X(?fIBc}<3 zHIuM@u1f%Jp+bid!a7D~v*{iIv}pWj1whP5Bo8snJof9%X3x;AiGYpm5f0*IjtMM=D?68?D0?YQ(94j|(hzv@1?hM2! zB?+rba)zV=Q(`onswqPKD2h)ld>WuSeK_-}Za{~$#L*bw^MQ{1+yIeOA>>ue0G9+x zA_EObHHWl^Kw83+5P?+O#CQziRWC{6OO11YG?7q791UgCvm}&-r3#l3t3fISp~O~F zk+B-+%QexLJ@NSB6Di5%sumuF5vb|lDu{%fj6*>M25uf@kEYrombZ+9k0ybo3RGMf z`MA*!Kl&(3ji>P7hM36Y;)D@kK9ndhl%<;Qr;$3im``MAhBT?_QKlDtOjy9s7WhEt zSh-JR^MPVu*{jL|-Ov{3L$h|f1&DUN1@^`k`4wv1>erMsc@Uo9% z`;(NnnE5Y0dv8Bq2ASdIKYV-ESB~EoQg#^|YTkXD)kjCnJFn|}`H0Drjz4@m2g;za z%h=E{e{eqe<<(|M4xN4JYnRPe@;bBdZj)BoYqRA)x8Edq`OFjc^*^b7cid=|WfuLg z_m|;KGn=c(78jeVCQJ~rjal2>mT86}Bbga&A&}~`*jG+Fk3agc)c3BKcIkP3zI^S~ zm!gr;DKJFr%U#Zw&pzuL58r>E6Je2Vh`DYJ^SZ=rJJh{vhs$s#>@3D~2T&uhH;%^j zZ1=idx4!46KeF;#AJ0DP>u|JJn|<@sH^H1ZZ@bmz@Uo2VMk25SZ~kY=LzFPA`PoZ? z_Ip7FJJJ9z`xzO>6jxd1uEiHh=27TcH!WHIXXYGyXP+=;fMa0IL)0+W_vN4pT$UWq5{Ov7$v zTd*7`I2LDzJCepzTQCn;=HEKwBu(iRW*4x(deX7pPzH_tL#mHsdvh6N7Iq~YUgqC& z{YBSbb?(W>9dgDgpPhE`H$Z0Buzl~#5%c{&zs=8=pMLCKZ$mp-_6oEANm;ho1}#-q z##VOAmYG>N%c}X=II^gT5s#h|usCwjo4^H(HT1*8?vab0-#pJp5PeyMkqHk0kHT2V zB|aHM4uuME;s+Ncc~qnlEj`56Jy3?!Ed8or1ur`SvjqP1VC{Cc#Z+j0XB{aHF&be@ zJm2DF?G>X?%NZ1mp(F+gtBKWtEq$V~=*z3<$i&DSXXb%;PJpPAFv%2=OJo+EjL9#f z;&ddvTGw)G!YPt1o$AOolf}5!mw?A^EgAwU!eW%y%A3K18`&@?Mzs**aiXKWEG4Fn zBJk925t~;z=d+G`j1iE~_ibEgRJt3C)lqw?K4o&W>QU_!k@i%Oqb0sb!YdaUbJ3tk zY<=F)c8ikcSyP%rtC872ZU@ZJaI~bQh@=+(`p^I1EfTpLyBqj&A*Ky%jdZzU3mTI| zmy=YdA_HsLtyeVN0AZdH2?62t839(heF+4{uRHJ}X+U;@4f0S0NffHBsgAVBh}5-M zQW2F2O)aX-#-=?RdNhp8XcAvTu#1?8bug4MtcPg9dV)u?=?Uo>jl3dNO+4Yn(|~Y8 zKoSkBNV16(07<+_3K0V-_djq?pIUObJiNc`ek>>r%bJW6#UZIYDocsV6)%!J zm6*VMMT;p(SbK2-C7@*aPeIb6qQFEYSHf&iFHdv>K_XIvSe) zKv%OO#*O4AI#b-JImSnIfBEnK+ycGnr#2*jv0#_mP3&EE*w!mlB=mo)1ZGc$mto-r zRKa>O}2bl$QLN5i>`BR!gO=uQe8wg$JP$^YV7X+lLL#m(+LtEu8X!**4 zd)k_V{*6|`8|DVGL5Bd^RdENthQA8Zu-CinOWOmti7iz_$P!xt7@UKq^;;R0ThZoK z=7Gt9so>N1w(?A{2ncUJq+Wy6TELN$-pl|=ORAjG@mC#`0-A;|%n6GJk@%Vh+agt5 zlj-wtqVBE3J2Q3CDj-t#&`%5jsbaccA)YYi7PgAh4MvL=q?`t(35T)gm;lZdua1Ny zt*V*xsR*Ug^#Q2{BWEI#R}8@tSR2XCF5(1~g@6LdY-sEG7|LjvaI}G;QYZ>5b0Zll z2tBq{mLwK0))cMkNUVJwNx!0BBU-K5fyM%KX^cxUA|M5n!o~|nKlA`BQwSH@1)`}- zD&?SrEP^u-N7IPN&_})!DNvZh1fJ!LdT(TS9b?!`GntqiT*h_>8+^9I20n{~m%R)^ zFc7IzS7=2M%FgH9$T*p zG9S6mCP49Fdu<3cJ4)sRT+H^duYGq|!^_Lyvq&+8-P}HMxAhL)X{}=q*z_C6P5#!& z2l(FCfxCTr_2obGeA%i2HUeT`pv3~FGiTbKwd$Gw?WV$X@FNsy$rgZ=FTeC`hs!^@ z^M)V(;QH|L%{N?)4A}hc6_)|X@^VKUa`2tE{lGHnSlR8Yb=FwTJu#=rZjPOD;&F?O z?_&eY2pj>x%PwsDTnWqxad*Ca-PKn(bjHfDGD=|Ci_czamUsJYwzQAR78>Ag2{AZJ zGsy`<5*oI_ODafY@sBKu4DeB@j_|BpKJ(>#d6~r)arN5yvM+r>$6&HkW-DP~he+H88|?iVrJDH-Nr54IYh_{#tcFEF>cEEqV8JC7 z*;uM*l%AN7ETuNXi!pY5;6=>(i<@t9Umlru89pL&cV1Ho{{vsxr)W(-lqOBEoSNca(d zU0Rrmu%aBdIP~q!Hu;nfj}QhLrSMK()SZD!czn1TlrM}S6(PWiB#Kbwl!+~Kmnv)m zxF2FezfR}^8?t6V1xD6Hf9}cX&@Dpir#)Bf2HM@h4A z6(=)a-Kj;Sw?ttU$H%_2bl?3yfAFDuz0nN0xQ4x8{E2LV&X?!CGzVo(k2AetNOdI9 zd<2kWm?sR*92%u4jvA3Ex^hLT1z)bDLL;VugPZD?q((F&BQoJCSXiXenZC?sPJn?o zxBvnp*&1O3*3!eMwmSpig3LkV?o_jN1sJp}TUeNJ%XQW)$>EU`WFT5bxk?f<5pjdk zE@K1BoJtXoukNZq^TH`y1tswr|3(wxRfm>LtrlNB)BTVk7Y)N}d;``es*G!|w~x4u zU(n8!Wl=MS)C)fKR>nb7_JLCEB*V)p4#~0uWuZ)^)J9;J&Iy>9)<~jP>xu+GBfuXf z>h}bsYF~{jKuF^e2O|*@aggmjESJ74;yZ1(jn{{Ljl@B-CEF!!eI0Vo3MGWDG7g!^(vtHsn~EuuzRM3v>`SUzwrZCG9{+~SQ~^O&!lNHXdTM&;s6dt@-d!s%9a z6R%aizKwuFs*FdE~~a-$*Cv?*QxQ9*UlsZ`3KuHUK)dV4<1C#Nu0K>EQMH`1ci?G>T(D62uC z2&uAr#1?@DRWw%oR3ynHz~Kf;sFzwptg55#?qhek+Hk1Wumguce^Yllp&V*4Y9LcI z0!9aQu{=2etN@l^g#a236UA`?y(_9{j|ecw0RT5ea7_{B1apYXDxk4%fANJ)EL?Z9 zNAcs2MLV$tl&~Vtz_YS2uY%f6jp*63Ybl`?1}EcI=g}Dq)!fh%ejE6I;ne`O(h25D zA%mKlI+9@`XbMRsNlGRmm+GVj3=za|@Zm}fvBfQhDDe*u4lx6bR2rs6B8kL#kS&?9 z;*jOytXyOq8o3mjsPFgDC{(UElM{`wNEx|UML^*p;z}ta<~5Tf0e*5;m!Vz}6H-8- zLqQY8PMGQynHXhFR9aP#I?_pkK92p)`~fffKK5G+ zpBuUPv)_DW*6Z`8cfRb1*|9Rb%zt6_J-(0q_JYSxIC|Hewp{gyDO-TdzK`vE8D8FP z%a8k?6vzxO=OAUL$*{9SYseI({GD z$M*l_!zXXG#)=>Lu=8aXlfXb9n6h3w*@PS+HDM>4tT@}SraqQ7AC!X={YeU5hMMoX z!!_n>E}MGJ<(Hg)>rK~mGV2H!)RmE&yvOc0U4NbTmoGTyTaKG;fV)r)9s664kAe7f z=TeJ*1h}@U+T!!>mDRT2F1wG7lAY6Ur?9eT?u-CZb`0!O+YY9^5@l~tb_*x~3o-zt z4r-aErfayekMOvCZIS_>S*Kl?bneU&RuOm>ltvG$vZcLfETc#jQB%H`8Qlm4x=s7- zuR0_42%*rD<)(F(couFG8cSGquvZKk<~FH;IzHpAl&+zl*i&Hf&B@wsO@Q1+QG(AX(6Cjc)4%Y5ORuD$Sk zSD*KllaBPguWPS3_v*{O?SE3PWdGpCX#-yN+hs@04wt>cY;|K@v}(6z1uF~_YqupV zix+{gHPm94Lu~D9`IBl1=1UTvlZ1mvi(oE-;7{>)t@6vnOZA;xPnf znH)MHqeWSA!E{VG!&xT;A(LN?+To`+%=tJgUD23S1(HjmH+2y&BRk4sO$<&)?R{Q- zlBw@p4ANvM>h`)fC95yCY%@O2B;iD|&9qt-;VXhLl2n$G1rRd^_<*FTiEV7uF!Cvl z0Tx;(iGm|o#oiUq=3`paYPIMb93sftY>O0S3QN!IVb4e71Gm+HVww|I5eB3mUITkCq zDo$OTdD$*g1?OLCL@Kf=S1FiKlq!%}SOd`&{s_b)CZ{Ay^C)9F7oO;bYSLA`rGX5s zI6;O}I@+85)T7`GXdGT%u+aOwqpR8oocDPo04FcOG)i4vuTxg-OP+)wk3cdjk=U{% znuiY8>$IqYW2wNhBW6gsBpDqlIZ*Z~Ff^SNsm4y*;vDq8iy_< z{DF?)lvFiM(8k0CHTci6g&S0#wBaYcuxv9NgeRMu;qbC1U>s_Tj5^&(U(%zXV;DK- zx658thK@z*W-v7}Vf~HjEIqW>VJIybzn1mpJuh77Bphw4k*=be6y-bB2oRez)KQIe zPJC#9&Ry=}{E-R-28x*nmSCFK3B8fQ=5atQ3}_pGG$srExHjVgp6QMNnorSIaN5wZ z{m05SxDln7cu_um#|I}3KI}-T=9WBjuH$vlCvdr1Cr=zwZ1U8O1zTPi%u9eraRV% zIj1RzHjwE>Icjg5%B~{11P-P++bMduhSM~PPzwY~9N93B+%FdmSAC^($PEARvPL8) zpQs8IP!yCR;_c!zWS=T3aYFD6MPU>SwPrEfxZV;qm)-y>N`U>R_J{guZYF2A|p3I6LZJ^V-S%s%DVy>{7pbw|uTk?mt&Q+D~J``CWL ze9&&|dyyGH?)Vrw2AK!E?0nfNGX&kCvv>j6`G{TDJz(3_j^1Zuc=@6;kGk-TqmSC} zGi$8)q36raVF5$)yv4;XE2PE*1=BjcvUWg@RudMoX%iT98GG7g=lIyyO*dSA%~cm) zeBn3YW#`MtpvX>#z0d0~_o%}Tz5Ui(d~(F0uoGcxI_%7DvA%!p^wGUX&f7AsUORE# zb;s=-CHvtN`0V4@fUtLyk2vH&G!BKm$m~+Lzfk$`m!%oyZaZ!7$2`6w;))l_Ry0PJ(}Bs?7S@V!6YaP3d5u;h~Pvd?1s-j{$QW`ACW znqAN41VnP~V*|@xVTPB_KJz&LUxt@&x&BgM+4-{j*e+wwc=Q*3BjtSgrRSdV3ubGd z$(fO6owCfeL|WH`Tca}q#aQAQGcC2k(U-+i*y`C*C^eG+P1(id#N^_T4N#J>5`{6r zc1S{B68m9@EW0F2iPDE$GUk-4QW%zlqEzR0=WDt&zU5sMTPnX-@tk#JUu z<&v4_{dau>sI>9I@KX>+VBB<0W=V3AN54X;6M})5ClXPsFR8>-E&&v+#43nDl@%u! zV@CgX`-0exRk8Ge)!{8ofdm&D8D_N+p&> zDuw7jkRwZYWVnvM+fz8>5KQQ}%?XqpSKq$^?i>Nb$N(~^%gPN4cL2on1&SRpbB>Y` zfWZ8&aEmF@51Q}#(d~Et{KpUZxz)@k`|1}~-ekP*{(DR;nudB)2vwv6kA&B<#MZL9 zkcy-__cU~#hL3nkP*aLR$zf;}_6eyv2E(3pq=GCTKD&tB?thDGe`53{I)9S#T9dO~ zNWm>=1XM==`ZV>KFtOEPfJAnYL2B$sC74M3MWXpYheI8yDb#Qv7*Uz5>@NSgaDU9M zxJ_^PdQXGG{Hw&uW6W86TtWW%Jo2O9OFPjyiX8&)nwyHVy4nVjlqfYT(&(Hn{4 z4Ms!Nk-f_vvYSRQ*QvaKW`1yO#{Aj+96T|hB1*FhX9$~od`A>UHqFX~Ej;TBDq0X{ zb27YaStk-<%rmq~y?QmU%)T;xiNRLyt%(cZ6QwM)EzIXYAyrTi;=+>9XbzDX%&?xm zO|U4pwS}AC76aYL6P4uB%8nJ6r>2I2&HLmGmnlf z3o;z2$Y^aK>&fIHq0S?tnlf)x870-J8p)L+SNsJ$ij+n5xumkn$gWVEJd#98A{%H~Ftet1W~3;-;vHMGLuS7k}mqT&>yHv-F)Lj#CIE+{>wG5a~3{z*A<8Bjn(Py#oP z(1b^DDb7;Gqr_yy?f2g@X&C-?&$^fmKHGUShnP>mWOuN;n+-mTNrnNc3N1v3C=Ec)0*R)Vq;bZuL&QwsB0H)MMy^vL+q7$QvC&@&-EH!Pbcsd!> zf|!0LK9V0>Ste?4VvP7;T6K)FLW|GGc4Bd1Km`qvVz?gP?<*0X;sum~A$o-hb2T3V zk(I?KMmg*y7#I_M2vxv8Qaa3cT$7W#`N1oO<|SdvCegavzG9p*$9# zEmNkTqgWUaG`1eG!=0u2QV{IMUX{cFJCq7qV8ieq=8>o zvX@_asTG%9=A`3~yX*GboH$>4(FI;>vH|XXH9+l3_Gu@Lehk5V<&-bm_IGyd2A4;u z8BDgwiUFKAo3xQgyVhQ6MtT0(--MUHdg@8Oqy`@__VgG|x*`m`w;Cd|V*I<=fJN^R7e{Mn_js$EcaFALwwi-Wwd270)gT14VZ6_!hNrx`J}5t!51 ziUEhI#p39-O{abUIBrljK+Rl7fR$5Y5E~y1o#Ug2>*Me*A-LGy7haPd4>6oTd(%Lug4$wDfi?<_dGXa^jE3ZU;gcvrmTaOGqbVP zsz3{?S=yr&R=|?oBI_};P$bs^CrJQ@VzRZ$%eNHD-dZUMvE=|Ck(`Xee0il7Ks*$L z#<0b_$bs0}A&kE)l5)aguu>_xFjSU(4Ekqc{U5^a1NzgdJn*B%8CFO|oewn{3+IY{`Cp z|F7Kh&)~S{-1DCMKKJRjJ5ph;>7&-Ct4R5b*<^ zxDh18B?uFHy&bLSLOq?~K!C4FJWwH&0wQD)lqPK55`?+1Buwg&?pEM|6$G)L0LFKv zWM{^ec$jJUt6-wg*;wKtQ5?es=SHAZ(U40Af^tz>QG=eG;Hu6pM!|#@#FbodO95qi z_1YK~8PdV9^zwt?Qw~TgPt|D1&+rq+BA#)rYh2awtS}84Geh7>$mS0I&VW3`GmAl3 zM;|_+ry~#RmDS06j~-iD*m1xX0-<#949)Wap*3^Zh+M0OPV^M+ny?JiQ>ZjyXm^N% z`6ZBc7SFL_GgL6?$@$z!khRgLj6Kvc**&!|Bsdu+C*X|GQXnR$iB3ewWBONj%iJA3 zw-m^IxJWCWPQ5J`KC0l?=oTnX^Yo@zk>(WR6Z)$(l2z!*2_))hI3egAdX8Hp6(S&@ zQbI^EUP4~zD>soK%a>)!vKYBDh@(A4!lINf`(s=P>ZC9Y|G_!_7*Yd&LbbYr8)mM6 zniRC^>3|6cb5hc+dWFZ>3t|$H0-wpc09*a#ijS)@;rU!HAUt-^rl^QBUCm)dnjkhV zch-?o;7)=h!cLi;BMWZDw7rb&9J!<9-n*75_x`qPFJ8*ErO8)bb;V6L-ylJjEaM~T zR^-k%Onnlcs%e;|Oh|1Cw?Ih>OnJ()-8i%j8Fh+L$96N9`C54ozE)?N28NNwneqZr zkg!JxR7CA?PWQx0oZQ16r69HKt~SE9tW1y>{D~^6z{1#olximPeah<3lAn%QUwP!pEQJie8X}+R9z^vQ|C9nM!KY9E2d9 z0;2A!RUfMJxr$z^D{-UPWQ0rw>IS*7?IB6;9?KK;f)Lsu5yNXOoHk$@lkB2{`Ng#O z@E*>za;xwg9Lf-M;|*UEP(ijzGdWF+Vf|u~(jhC-6NhH5h^XXF-@(BXE8@VV%z6zU z5>eGk0ezhmb-e~34(1qI@erb7j?(yNois#7Y_Q3B)dl>TnCZ&u9vxUd6lkW&Ak-Qm zTB}5h2EtMpUNy^bE17Z|XQIni_!Eao5<2xibf&+hz``PN?uX3sWlM@2xp%X@p6!&m z_pxOwN+O7>2PuXp{&?a{p9I;ZS`_ew`djk zm{-=g)dT`_Z3RPeYgMU^l}dz6Q(~L>t_lQNF`<5;7!(U20yzHCKmtTL*0$=)?H)kuK$d)gAAA8=uoA^Gqm$6kG zD@>$A|9Y7tNbXjmwwk&)nD%vlp}F(caL8=7V7J<a9qzpNy|BeSPk;AN}ZM?|$EV zF8a_1-udZIeB|=aebNRYCzbqq*8OWQ`eon8e$DykUH-eDmEn5B*pp`ry-n@6$sYBe zamtC#pqV3Tg9}lJF#V=WBokI@T&x!Mq3+H{{l6R~0r_8<# z=10ww4`^?*`R1E!BEhtzNIY$PmR4!6=#(K+VDW|wLdq-%CPJcQ&ypQWlUd}*jCB%Z zhrDdJ1c%nC%~?oG*JT+pQlmL-@>ZUm{=jeQZa}AHMW0m%aZY zhs$5N;uHRb+5bs>=Z3E+x7~D|A1|+7aqr^~PeNAPmhIARWf8W`wQ1V>Y<6v70u0Nw zsid|2&c0^XYjVLsqRDL&4YFHBQ`azEK?;fO3z!6lkDDgU0ad`&(Lzp;xm>H=WQa4P z$3CJJA!Hm-SpgUYq43-5b4I(dEo{i-G}PBfK;$@j+_=(Ba8TLY?Niu7P!T-X+@ZC3 zaSkbDks`TkYoiRgMLU#C6ZoPISDM2#vH9AEl6kO}LT z1>0`5l@TSbqfCB~r3uA#D#A_yo?H2{COeC4PI@b*brs)AfhEGi+Jasou4H*cxqy8)Tkh;+ z8Kx@)v|h%>B;@jIRJb{M<^<+5iqBc}8G=mA!%@!=0Ieod5hsKar4n+uS(xMzmL>ZJpl2^0u~7-T1Kz zDHs~Je5h`SYT{=qsZf}nfIqibWRSqo9n`Qi8Nfy9#gAwR<)gzN{r0ad7 zf@q$x-8*fUv|7#5ma93I&W498G)b|?+7}kXghg)c3%aJ^-O8i8-6A22&K3vDk)~QTK|mW-9Raf#TNlXt3;-#!UZ9g(9bx<% zDoRnbTfX8<>QTOo*gQ%ui9|3HTV34vGzUd8ykUhz1qgu-!Wd1uK%KS}LyNHWL0?#= zh-ghPzzj1wa|J5#7;ftDxdKD}cZ<1kMT)vE>q<8>$ckq<#XLiV{RY3Qq|&@*xOb`z zMZrH~c>~;#1LOxjQKZb^+=UPtS8$Y1ei@cXKujo9DiBI2${^fo>OBh5lWQ8vvT^7a z%9ajkvf^;ra^W1=f}p7M%Vo*3Q)d6sj3?&!P`*?Ma|)D3kq!<5p~6wD_Ap1RX#C`A zJ*C&UPT@nI*9*E}(hFjG2>9gws_vNn4$OjU%*8Ybh04?W?;7Q&FI zNuBZ$5?-$!#)%Lm0=?aFjTZNa6o)zKmy9!>{0v z3Ed*ka5%9gD|_9-cU9Opy*|7O(-1PcELKLE(PcR@(i&jK*z=1&lPYEI8()r+`+KSB zXUfyhntkSL@};k59+_00oE$OteQf!%?|nV`;9XCyy7vc<-lzELm&Ez;Rk!*5@{0Rz zkT0)V{;mJ?qvdaZ-LW#|S-Y)&@XXEU&DwnKo*N#t*M<(4eeBC8vL(pQk>~Edt|CWv zpe#EciL<(V84z0UW54G38D}os>6~Nsc>Spdyynyc&RaD1;2B%J{8!d`zAUu%g&awr zEx?vxcb2tD+3dW|j3jOn!`^3hHtOipRcdSrb#m%fb_v09+MCY;a&e>0$V*c~Q)#KE zsp5gsOfOCgw(YZh)Y`5G;`~T>+@Y2I2(pmdGV7Hp`x-Swp?LZ*CK1wM2`soYp$wGf zn8<>a*S`83aV(3RF(H(?T4w17HpqmlTj z6ecoj2qmzNy=(_CDdR0H$ITu!FbYFC@4*MmX81vKXF)*aL>?0)@)X%LC2ceyJcFDp zkGrYI?eOin9IH-4IdbomvI3k3oTST_oiCrZ=!i|%e~Db!``FHrZ@T^}pZRjW>^IB( zY`I@CtHb%i?@Ur=hs%Dy{Gs=~*hK7HVvhs2*-bCaueM+a(2x{1sMs_^wh4|B1glX% zw6+(h+c$0TSU~{CjaF^Ya6sZP{R$I~q7poEwA5gu{xa&ym+eQ1D4@jUfMZc1am7ke$xE(0292sLHB)%Z5^X_d zAh*kEO>LjJw;+Hcw6>U)!Z6{MT-bE*-nHrMoY(SB95j`FN!>9JWq6Dr4yTnx3EaUG z_sVMJWpW4NYDM0x_pruE zqk@BjF2)=@KT`&>@xTfdSL7iF>PnpPQPAQHO%rZI zlk@@CikxWt83=-dR#!zUmCQ8|1sdcSs?d$q5a_@zM#VM*)nU47#T+-}9P5^PsOX=< z@F@lex#6dd8{{VP2n_g>Alx)5_~V%e^d@fDreRGW5Y)+1^qSk0%sQgAl8%r|F@Zxg zLEZ*2a!gX!mJ!OJH6ZDuugy3+i)=9^DW=eh8zImNXSWhG6VM39$KGKKdy@fGQ-B&2 zb_>7gxb*kAnVfAL(x!*Ivke#tLq_Z2d) zjd@1#mw)yrqBuovf;K`P%MhNS3AS|{w+!DlZ0OH#T0Kd#^4MNIZDt^Sa9|Xk{GhHk zm4E#Ezk$OKrkk}zV#}Edavdt+#YC%+z*8)6K4G1SX-zP@WxG~Oi;Tqq&vfvKeqCV6 zwR%`aY~_|3YI79LEd`!|aJ$EqmNI`z1(`}M+Bh@<;Zu+6^fFD{!fC}*r-NHY#nvjJ zFF1z|b;`hm0N5-5R0pvNSxKyi7BM~{kz49q2>8T`6r;rmq=qh_eB9`x4hI5A%sB@R zR%V|y&^|@;xEa8@39G0Os<2c-(X$b6hCB0>sLE0W?8ajUREo zrM8zq#;eizz%QebMDQSnc8i{3IjH@yUlVvh`s@?l67k&tFMCK-rKSCvxa`$4F{yNa zCMCBeiy&iFf+GQROI3L^O;pl^;d+4w6cdm)*kuF)<{1^8KHSL3My8@?d82N1MQh_2 zRJiF`x2|YMkJKs2A_Y~p6Y89BwCS7a>-|( z!j}1%1k?&14py4{&K(C8B0y-R4ksz;oDK=?8Zvy9FoT2869+}btw*3G@98kK?GO8m z4KG~5BoP8CuE0}W37NJyq!=lgkWb(nqK#4mwGzaz9L1V5itl}SA6t$rDeiFDS+flK zkN@!5Klm|{N4xC|+PSkMxhEgFU#_9Ddc|VJo7g|_m2AIaUU|69q1zsQ&=w2kZaQ!FM)PKG=5YCl z{kA=P&Z`cay{&TO{@YL8ZgbS!?T$Nmrz7^?`oKLmbo%UP&fd^=tbE#0yPSOZjwc~mPo zLF9T`C6yL`hj9`{C!umlxAHzXox@7>+)6fWB9JAFHV%QbPuxfb@kAo2++x#B-~5Kx z+feL8p7Fw|NE>C`xH)kKaT@Dcx@W(h58Iw*&Y0nILf}4dByFUUjnZe12(mHJ*~Yi- z%U>3``qwbdodPS5G!;eaOw0qt~NlJvk| zOm1iCNE%N+RJ5Op;e|1sVsEzb1`-D$q8wZl2TjztOtueEdW2_3HJ9E_u6OD?4H?JJzaz zco|!=?DxyR`^iiFe%YTdzvInkl`s16xRt1iq??F!On;Z zf-5F%l$g|td94CJ42Sd-(~1N`r8+4xEaS&5e0(%v9)u>D42>X77O}HVKV4ufXf#=b zZK5fLK!~%EX5a~B^`L_fttO+XB>g#=t@kt>$= zV8J>-F0aCo&r+MiIS>bONs*fiw#`IOx&+DGB`Bg5{8w$gwI$9P=<$NVNoQ*v2a>C6 z#PGhU=s};lP9eQVBA7t1;z^bz)cQy(wsa%6{E>7{Q((GUnk+s>h;DMLm2;@a?RA{n zhRtw0G(gH=n@@;P0aQ_#AQ%=!hp7|C*hT`Ov8|P_QiBKu6E1>-11b!IAfXscR|Sx6 z(CC1bA)%6JP1r_70XSh|3!VsmpbvZuiRg+6H>M2H`2oHP=7vx{!5L2-U2&^GAVnbp znn=L4(n6H$sEQ#-8miNQCUiiH59tc&;2^FDA*%pJ>k1N_T=lro6K6w3j^jn9soy}o z&~(VUx`u&mUOeEqGZK2@hPFsX9h?dBWgCRSx!pF~c!q4S8SR)GNW4K1>4q!hOy)pR z6s#a9Di!sZt3U%pqT-e~dWSj3#_>nxAW?fy1#x`F3UV}qUyG^%-kfvjV7e$K<{NC5c+gdwg-l$<}h24)_^ z77hhC&1&OuI#m@G#RNm1p(M2U)0rlA9KB$=q2(XQ7^)1d5&;fkWwzi51!*1{LJ}3^ zT*Q0euJBE4Npatzm*JMB7WT{aq{H3`^w}9#4^Iq?@){xiWJOrA7>S`G$QRF-1VNB- z%nQIvzLU2?dFkRz` zn_KxZr`3-WFHI%6u39T-q0tZr$Y>?SD$~^~H6u>ui@!3+Re}tBw|FL09b5^g+vz4Z zR4@T_rAdU5P*_2g0|Ao@0-shxpTU1rVi>J!;|B+<;szlp@x(3XWGoV`Tu}w`AdG6z zMukb3!2wggtJ^h%4g`5LT4Teg4h@H>R4AjURJSIp5Hw1P!7)}FmPTWxVeGmFyjH(o zmNH9<<-(4bCCDni>!t1mY?VIz)p4_&TC)7~_g6go*h9}e`rwm~EPML<_y6drhyLJ) ztL4kS_x0$?yQj+c9((w%iZ5m_zSYk~ufO}YtM0q=>c=0xNxpo*J{!#2Z{tJfNR~G` zbk?TxXKuQ1)>iUmneqwqc3*VpZb$C_s`>kFaoFswk3VGRMe}w#`oLH9!nSv_{lHnK zT(W%15j&lI_%2?__CEGG$ILk6=)I3QaHkpDzv37CpOgg6GgZeWw%XEO>9F)oW-3V( z081nld9)l;=IiuRmRE)=*RwC+fUvV^yM~C-w&K@?;zLc=a7`eNgeKXzeW6k}mzUA;!mn znmEuWv+R$4VPs8{_(Py1-8|U$zO=C4YmjuHn;e#sfb8`vNX+FLw1wkwfmm87PL?f| zUHo8sr4J2l$Ff>6FwMzANF0WueN@hf<>{P;Gfca=gCyqgjKgK375gkVcJ|Hx!gWyr zSN|Kz*7Hm;&pbI0OuPqc(|NXKz--sHWmKYV?`V5!H?jL5!2trE zqiBUTwi}hGvL!xnP@z@m@0L(HGaP;#?0w<{L50vlaOl?H(RV@-6;!Um2JR|gix4d) zaKK%;kwi@u^`syd>cGTA`ctYBgdkK!_{3oj8M$8IN#vqLgx*+CjB{fKTZa+FMf`w- z!u%s};v!b~ZGus19I^$nRzk7m8LUQy2s#i-tJHR@;7lt@;w(y(bQ|8rrxk+&hNuTq zCW1Vj+ElGaRPZ+{Xwpyt$wuDh`FzfYCW2>QkcAVJfeZ~>bIl2nIWA5XU~?(W<)M(Z zdR*WsoF{ny{_p>W8!Gw>YduVJJmjGkdvT*M8x>laN~W_%Po8W2!{7at^CKk1v?edt zSPt&e?bV&?F?}h^0QrTE5DZNvKQN)fXUFUIVQ66EC*HeapZ(5_x|-? z{8x*NF4G-2b26QI=5PM;Pt6f)g$eFglcMo|{ipxV&p2ZO8J`9fJ}Q(kt}L^f>LxwK zb=1(B_mH?%AS?W=xW$vVbtS9Wf)HGtjey)sD{y=?spy52ho&xwGbm@Mf`kjm&j8e*kjl>)pCpF~~R zBr^7Bhze&TjUeOMZ-wlVX1zLO9!^zPql@OQ>f{>9MwG3}tTY7_Y<(5KfFDCT1HwUhK7(#SbOY|hk5myIK?Nwd|kw!Vn}@c4#{c3UrFnC4zkX9bVZhw?C4Y9&;7 zz+*jrBM2dKglBsd2xJg` zfP))bPyymYbA!rAwu;gLb4#V$@R3k5H6pL?9J}i~u4{M`1V@uwy#Nzi9CQaDo)#SZ z;S&l0#1*pX?+T5XIB2!X_~Q~EmT@F1|JCDinX)ZSj(UCW)lsQm;W;ceDm1wY@+Gak zRxLL~BIcL!C~_YI$tT}^6;>K91y=kr!mG{>mmN2k7t5C=&OY2HQ^2<2f`7&8V2H!@SDZsK`xo?$GUhK(ynxwSGV@HsPYa7dnq zGx`@f5xQ>_kh~3TxG~S`eO}^?GJR{uOyZmE@+3`cQykvFZwe zAmfBenp`0(;Hj!oUF(pjP7_K>9wnOt{`?AB%8-Q{=MxkyNaB)$(5+U=Ah8zfuhQG{ zaji%)KiWuKaUeob2{@Porhf?2{SXzm5DMSzXeA4uNK^(lIGQqC1?OA`lfqnKFgI2k zmd2`Ez{a_X#i8k-N$C(-b}ValrtEXsj*|Ugx#MP^|MFImFMtJ^d>?zoy}pv_NKetbx2^N#MThQEzU-S| z-r1HfJ7PXz!FDp`(~sWu%wuYHsm$#YS8+F(=mpn#2>LmYR>sYT;x z?Ll1RRGPOrMDEN$5b3#FZj^@jp(qzIQ}T|A=2w3HlSj>;`|@>OAXAnXyULOMirG8Z z-p%$CW*^FyJ}Z4O`^qnT)c-HP<#ngN=k4eF^X2kohs)P|=~EKt?|$=Y|G&KC&TlWi z?M8norD9vR_$$H>+qbEqVqTag?W%6A>SI%|pIY1P3m^d@w42xwa4_$pHRDvYwuuaU zw}6^V#=#aaw1DbqHLet!PLfKO_- zl4S>%0vlj5O2d-!*m@1ml);LI22c3rg7=ntb|h7#Ck|STJrYqB!l_h92Pcls*ybNs z+IF>|dMdTaE7$k4VlR*w=Lhb;zqx><@D*n6Rl$=)YJ=z(Jn;uquR9Va6IYBD9SWHU zzktBGu-~llt3yuUe+FtbO|;60ciVY;>(!r3eRm5vS>hRshKa*Dp0>fH6_ceKB3o~* zm_!_+0$D;zqWMU9(emH^^`CoWx4qwgZv0Bm<@AeI)6;PkS8x;@lT^O!`J1yUCSW3Q z=5y{&hh+QQGMMWp)FvIfn9V=Z!g%oZdV8XM%Q@SgbGx*=J%1ksAS5H@b} z2{cx4V8c?XqMLZw2#2h-3LJi**q8;F#D@+{KuFt0nvOt-9NRh?91x_z>{bUMrvo@N z>i|CFYI0Rr!BO=N9Y}|dT-_B_I7TBh+%U(X0>LfJlvIZdIBwMyRJwrL21!Sgv7)?I z*Q`)aC_Yr>iVO_LEpOA4=B^@tDyhc=36IeVGFl}#hNOc-$OGJtoQE-tN}a+9g#%t; zOXlN1wA+fh;ceQQuyjZ(BWLP~){9u$NS3A<@KRvoUlPaj1~BzT&(f-S1Yd!mFW}J7 z#4!ad4t|Dj_n{sH(LN=0bP8LRkqlb(3bycZPEan*$>4C1yET@qGe)?eOSIajN*1K$~k<38_o(@G|cmE>1O+&kD^}M>oJ%2 z%;n3rV9%hS84{SNU`st{@C6%w=FD&2*02I#>l+B3hS|qGfd|*>hZ~;w*`VPN#4{dV z+2SGC`fbF%fQ=hkh4+lQOq!vq@L7x850^=Ueu2 z5!)NLysCf$)(SzB+vX__dJTVNbh+fw_?lS3UTbZ6rvCb?5 zFXwU{`H_0JASyf{X_6vMOn&Z+IOK9GBEpwR4MFP9Z%qN+q_0V@Voku`HReg5etkYX z2ocaocS|mAAZ}F%(lkBagu?_tn6;)JIQ)RfHMs)9?3#!?(0MlzKexa~?us)i7U8TO zpFt95K)s{zLsUa<5)n?rsnhE}#< zXfA-0ILYg$p4wSe1euD8iO}FQJ{zteCW5A^RuB5TgW)exp8BJoJS}}zWX*tbWSO!e zcOL&I_5BBz$d{iha%Va6l0J^@_siwWlI4}lZ+UR(cUIi@ZT~QJ`%PDSAN%P?zb9XI zzWm;|9>3R)>m0DpD;zN&e!%7n_n&;`OTO$?>eG+hSJo`&J?)6SPd$9EQx4nn#QD1& zId>cHV;^(StByZ(2cP-!%jHG$UnN;S_k_Kb*PXDhSF-(n+4-_Jx6e9e_NhnC+UVC` zw6^!L9SnI*QIaUGbA&2*lbuN|ZPRv>(qvazv!WHyLs}WGWEn&zD6Ix`)#_y@Nn$6_ z_>@_TX}u&VF&CQ3m#sqmdo0niNHHyjc0ETt_CW!sqivW}^6edQul>tOF1Dgvk-rpG zU;tSLE$sK_vmVRKsU^Igno54Dq}9o?%}Wp~M)eU78q${9B2t7@#nAaL&cr$TQITAT zhbaS71r=o+Hc8>VJ}t!Z+9aO7ye(CyA$QRYq(dvlih@Kb^g%)~FKtf!t<{U1GLyw2 zuEfEIsg*vSRM?tTWX+s|r)^zb>g_23J`xjzvVNTT?UrwSb>Ts?)_d_UIZc)?dn@}r zZ+pGe*w?-K*|JLMGobV3tH1bhpU8gWg(to1t>^mQmm}sYzwlc=j_qHiZuP2sA)AWQnX&l2JyH-CuNJe*`2npdeN_ zfmV!BJb>0hfe#KKNQ(bO5f~zb3?~la2b*ls5#(VY=F^MFgaXNO#nUZXOz@-<=5QiO zlUQl8SQ#N6Ng1F1o?95>?8BO7zm5rIWQe*??$#J~DC0lMS%JUE$4nuGTkyx6X$hi0 zn&c%o;Jm^4_`6yqq;=+$8Rr1&tocu4OSQ3UY)zj9X5iEj0KQGmE_#Y9%zC$;Fex2mw7= zwFenRa9AhJF*fWPGC}-_!w_2}uI7T@_!wjAXf;hQfOr;%+cFGhiW#&1zF zbB+NPm98AIIwlH80TO)3Jk!u(c*cgNiA8n_noD7fQTejJem;2aoOiwbA_17cjuPof zf4oEM6i=Kc)R(Xb=NM>EAt%I$@`n=T4pmnD}mCTCX6( z!IOIb^bbGvB|wH-np{1$5+vB!I}6}OliFOfg5l6XGoFnBnKihX>rD?EH^L?(pLlR# z^>DasRe|7{p--XeJ60s!3Ipg_oOjQxA^Ofz#yTU7ZoFsIv7NT&0xC`QZv&4GoIsok zNI>R*D7q>C=(xrPGp_QzlJ}WR9fR5;7^m8;Z4lVOIf);`ncYkN+U%bg|5KD7rfiIzS~qGifn%yyMK8Y! zf>YN_7X*2*f~mks2Ui{Ku>Fy^W5X-oOR4hS#$aFL3_Q@;d1%1 z?|mJ8(3XC}e9of1oG<&{mwfrW~rG685_K0t>63dI8<8d;KrUJ zm6P;IEhW6JGD@kgv}I9{xpQXQXvh_=bl^oOwxs*DxEIKoz%#NQ zK6mwif`>^gUv>+h<~a8oaOa8A6aQ8tN1nOI4!do?g=1y!V}JO4@9-1mku3Y_m*UXb zS+iE}W?yyrZ~oS0?|Aju$N1#erSE#(B^STuv!D14$?~T^e(BBM`0|oFZ}Q16C(FL~ z_0ZDq$(I>r2ehTO=h_8q3a-|3Q$|N9R&sS~KAcu{_;w1V7@!5eI01f}f?FF3RxyXE z;NTWeWfUas;Pwu6gl<*BMCF!rS53;m89w!~1U|_QYbQLX# zenlXftP(7xCk_a;f)^^FVi#pJwOwL@r=pT3#A$-oma48v;N&rpafO=^OQ=wU2%L%5 zihmm`&V1;a6v3N^-X1`S_lyvc8+<9VGZYys5>kv$AFLCEd&~kyjAy`;LUdMBJNMEJ zTd|BnXK@h)vecE;AVlj^v|fI|P1@+{tqg9Fz|sv9X*;<{aH9hfr5@2roU0NX8aEPT zBBn|XHHuu!#nrl1;W2FS z1ji8V?z`-wF5Wh~*}^|Wd-4Xj;VULTsiu!*K@a}VfB6RpUJ>uHfN4$;=OEz_oQ)9f zLav)cXqrS~#l3_Y8ywQGq?=aysOSp?i2^}s2)RYVOE}aWNa`I_L9itn8dl`eK|SU= zMFBSq5LVaG(SoCOoet521BTY#<_pn)RKg@oe27r8*8rd)(OMPiLC$bfBW1t=Gtiah zN)V~OANEZh0Pu;FK2 zQ%|WB3qfEn@6~0fK)8-K#D0Kvu3l)?sNi6nj1r+%JjvBc5MU7Gc;-(ga8xP3G^Z?V zWI!PDoeIqI<$N#{(R z3put)PZmB&ABtfZPjEnJY2unPID7PO0dcJ|@R24VqJU>bM^8+-`Kg%fhhq(E1EU@9u9HysBsxF#Z? zp@J|BcMGT$P>JW@)2%D~tWb}fKiNWISH*2()wKm=+%}%44{3u|akAV8v1QHoFTL}Y zZ(qM;@h!$e`LZR4skC>qS3R`kM^CQu<7MCb@$_xB9{F+j7L`M5*2 zcep(HW2reC`~9*b<`Wlg|LSA+I%C0(uRdX)3l`1tGiOQn+&wmX=?mAIFWX$K{L)5w zuFSC?_Bz1n@ifiAzlE7g|;PTSWYk*iigB z%e_T6k*vx=WB%aolyD=OZ{{^)O9 zblw?9UiA9Y%3OGiMsXQRtv7En^WdmaJMQ0Cvs?LM~M)Jn6!6?gM7pWTGdCGp$UhmT-B4c27ENB zXrh}d5wOt^nw{t4P)(qii*8bkL!2&EMz323sDdfJqw>t(v3>+v86MIKya(I zl0L{a5uxDL#)`A>W)fpe<=3uSp@A5^6fmN)H-)JR2XjE{81?)L&r1k$VFBbJ2hK4j zO(IX6ymkm^w0QkkMrrV1$LJ4xhbzMturR&mBK=S zfh664fiD4+szab^1AyU5u^(1-Hy5!{NQNhIT#B4T5JpjwUBWZwP@DeFYs@bb*&Lya zR{SB9oQj-1Ofx$i5ECJ`6x&$4ysL@U`IPE(@+3Gjc}bXU{BSt8SW9v9bpjbZWy}IP>rIh4 zLvYq)aj=GXL}~f*Qwx?_z9`899(NcsoW?C2#ZtH20zwu8xz4S=&}BvfZxg|>n&4-n zD`akOzf*^xH4gN_)-68ulq+DIv8AnEsJk`Q>J|xcsBm^=IA9e@Hg4g-*W{K;S~bNS zkWqA2Ng1us02OEo4-n1OAjq+TnROrr44mLZW2;`PE3L8=6+L;ZHAhoplDITMrGgb% zO(;#nY4}tIIS%SnMM5tBn6vbuTlNArgYpS9WXfBEN{95?g1b}n*dPC-}BoPJF@B@@ahK9Q#w9_BUc%vO_~pIvFr`L3*U8fYt* zx|-(9H`A>ZVuo}^?qwKX`tmTr-+yq<|M|CnrUw29Ektr;55)XJ%|7ne8HNgn7%J@L z2UHdfO_l;w9(e)sIW>s(B;2y&ms1vD-1L!Fo-`zk8LI^Us|ZxMt}do?IMmDkJj^HR zyy0q{Q2^KI6l{_359X|B>9!zK#=byd{2|1gINsK3^`j~hn(!E_hbB!ZXvk&U9u7b@ z>g+@mwhz)A4mAjE?yv~Z+9m8!R3OW3WurR1tBUbp07&s?md#RZFi%hE;MT4;tr>AX-te8e9QUk5EMc z!Hv~QsT_PoVg(7WUJKnRM8Tfm_( zDXEIQEH75^aUE>c17?(ZJmcWX9GT!SDs#fYB-(DDe@!bSnjoj%B(Hof(ay|a3st!S zLWD>Vs^_$BW0l;YIUEJsFlh^Iv;qbXp}JND&q~NOfz;|YO}Y z4Nl`wY0Nj^4ammu^s9VXrtEN8S-Ik#+i&p?Cf|`GyOlM26T4TkUHy=G^}|d1GIsA{ zOO|~fTe7@t@wdH<{p4!DWnSidxnC~JnxA>>KKb(RJ-OoEJHB@Rys&D&?og|oIfdEs8pmz^vhd(e))`XyyP`LNwi zIed2?$DaDbsU7?U^Wpn%>dV-Un2$Sjn{$rX^@3yeeBCLt-*DQ2XC5=d=e2j);>Ew< z_sg~(JF?)d{817oag)Hhm8*&vMUuTUtO?vc1qfdfExhzHq3l%>D$^7tqJk!*?CNDm z2_R(QuIw1C)fkm@SfZ*Bfy%OMy%j=`CC7xR387_K{wv|NbUR$OPe~yy%Th?ll5S|0 zX!sT=e9~ONOhd=hc1d+xv-Mw-tAvzp2x%f0R`Bt7-&KN)vxHi~md@bFX>OBcom)rh z5?dM~#0`nuME;8{aVg-zKISyn3Qa04koGfVS8=#wXmL0lXvj3~;=K$ZSA|zOLE;1M z-Evf%58v}}`@Y*>eZdQM-EMOq`*MOTQ-0S)uT}SJ^4>#hfUU$0kp$vwGr4R>?yVeK=o#p$u&ybeY+0?+c!+DA>$mT3R*z7DsT`&w%xcY zHuS`40#qQIu&xSVMIAYAn#^n5)RWRQZ)0+W&nN*#(YpXcm{gL;(kg5$zz{aTWGOYY z8X<#fisHi26vS0=OLV@)9|sUt@G~2YA4A#|CjNBR3Z7A7 zPI~%_6T?sb<#h$Iu^?HIW_IN2;gT^UD^v_-zxy+8@e%xx%LDpO!QT}_;uLHpu!U7A ztm~-(M|5OQ7mIGT@y3R+0qkYwtl(j^G>4FqAazjP3aw7zB96tMAZ%I9IV94}%Ag|b z)PoMxM4lcHvCf#6QA!+S76^IXG4ab^`fE;*A()cB_obWgCxs4{6bpsa!wHdj?V59LVrQ9+?~$+BTXRX_-ajb5WC*Xdu2gSs9> zXf1V>-OEtwbhc1C9}2c{aN0$#N-v^_%(;#rDL?ax;~!pP4o|Ao`;9RWUL}hYQ02Ww zot`DD;Y~Dhbr1taQ>+jw@Ldf6`s2oF{G+(7Ygnh1Y4{*-FstZFIHE^&45_4mKyLH` zsxVejQTU2D3c$pN+C+ep>xvKpSJG zSxL(&_is4(F)S9Qpg7q}|qvJMuERR;vB!>45ZK1x{j3)7SwF3x&OyW36%?rrC4n&Y4-?u@rxUN>c3I8yh-9kPywW?2>w;^~SYF-2Tpzdb)#` zdWc~nBQ$Nj@j|zM{vUs9Rnga&$c`x~Q|5Dz!r)tN2%@k#1v6Xoz2EaI=6=Eq zNAVa(F-mbpE=RzVXh8WgV4_JkJ|!azdOQ<>Bt9m%rx|=m0X?W7gr872I3P(S z9M+aEBS*yq*V1Psn7FP1#FJ5gsT6)}A!{N|ldEK~_g?5Qv znV9pB0nBP>7|N8ZK+sAz3=ta4R8pZpB8E3&8*;XUnctwft-!3fosJ(cdvQ=eB8N$N z{X$7Pa1jUuRtzS2+ikiTcnx6VwxR1bpi0Ha97VD$DV8<+&9Ve}#d2Q=@ixxSK9sFV zamwt)>~iO+&tl&%(Qv-}%%czZKDNJ+TDkm=hnC&G`oX(>?8{$DJ+S0vMb`Y3xbl|k ze)7!9TW|d0Z8u-O_|`Aqb<5}9a^V8s`#Rx>9gjM6>-lpwK4_nf4wRad7p~)}$qps6Cd(S@8LXF+70D^I5>;8PUkQ0BTKsIG zM%X$mixdDjL9-^4STZg@9cgdLuUp)Z$U`wC0>l+C>6l|nD6JH%v=T}^SGviLX-+*M z2Z(}Cm?~PS1c-`;AQWRPhS(Ac!2z7?n?6bOIGX0da#tI@t2Z!-5XqCy+cBiGLeKPJ zAeH^RtBj;$ivi9bwKu2JC}0+OK;AOuB|sHP@tDTb}0LQn`INu6RO3wqfuHt;+|Kr3ue zM@zKW%#n~VTMBQWsrdD5xl$bH$S;{03QJEVVJAk0{8q z3pl9?h(A_jsQ_Z=YH#&irI)V7at>cy#mOr2GAElxBYb%Q16fBp<|;JAK_ORNgijm~ zKp?B$VKs5I5<5X)wN_Rj&+~dAm7u9mzN?C+Zg~bDfCs+Q3}AWPQ>gTZwlhk&@{Ro>=v8Zr|@ChJ)WP|OcY z@u#roX;uTG&2Doatr44lcm^gZi~cCnW~6@)Z_NtU95keN|IOHUB>5eCGH z=r~WmLPN{Ov|^nRV(9XlE5*P$jT+EgVWu0xTmhm`%m^_UjC>WcJRmi+3`hr*vfcbi zTTalko=rI?wOJVF%xoCPpE%4($%IV9!3K((^v!_upiH6%foQTCD)@{+f}DSH+NeOt zZz?rG6VjUqD&*or9}GoVf-&hv`Hs9s)+0jeh)^Ucq!dFIl?m05=LGK3DMlJ2Ur2fz&&O|;cy=|DwK9C*)3v4@9dC}Z6kV4h0Bl<@kP z11j05?lT66$Um4^G;F@u(hC2^j1<_Ew4`_tM>K9^>B?H^NjJaa@Jx*7g+^{6L{24E zAh+lV6-ac;f$7Y{3b}3pyQLe?kV5nP)9eOO)FHs+f8)V`F_5q&!bmd|DUTtoh}QM- zbj#;%`GMM81j6bDkDgUAq_M)p@U!M~oly#?u9YSjvO2{mj1>eA0c@96#mT_F?Yoj< zV_>AcLUB)23eg4aMo0&yVt%7SF+v(7R!oVZ92S~EWwgiw3@~t19>vHU@A90`FNvOg z!r`*SS-vb;{*LckE?um6CENShKX6v-DUe@cdM-4T#Nj|!1zQl}diR@>fFRNe8cqi4 z)DI$>Wqim|y6YG4#ysb9g&qXa#ei_&yB(!lt-RXUGZNHY-PV0@(AKK1EBRATVG?=0 zX6@h*lZ&2uy2K~fajfKve>jTffZf&#s7c;b>I7I$G$3xnbd_O2R#XD2ODA<4H2xu1 zPw5V~A+M45`M0=``If3p|2i?zsFZ}#?PuP8?)ZDC3YjgsqaJC5fFMym(pp_|);>GMyB*@ZdO%9q}{hyRWXTM+ev*jN@z1myZQfBAN&px)| z*~cI7GWJv7U*=z!pMG@d%H_8$z5AQX@43;(zLeDu+_~c3@Ay9U@_Vj-c-c1}yzi?Y zdEc4aZT5nLXKm>Jm*vae$9BFvZ=cPN*l#g*}H7C?ys)(eQfDo z*`?ggMo{J`cLsEo7ne4IizH>xZe?@o&YH!0a!k8}H_G1g&bQjrZ3o~oS#a4gh!heN zL9cM#qN;+GY*db1GK(C7TM1)St_a1w_(&D;lw+><>%Z;|D{HbQ(zU`D?v|<}p|GL? zqR`XTEs4-75>on*o#x5@I7=eApIsR=_d)n2>r?wne_ z{Fc|A;}^ zhs$5N;uBY0{@dTU<_qP^eIMJuFxzcyg0{sbj;pP~-f9x5n>aQLMJ0q5{Lu=oSo{Z0 z%f2i8HU-!C;2c_1ZWZ-N)I|aX8v`E@c~g~8^SAoY3MN5`Q;(->kebvrF^Z+d2I2=p zi=TCw=-0k4NKmxq3i#p#O(F*+s@PIPNtW=Va2iAS_`^vzO@Q2sT$5~(tm)C3=!Igg ztcD}_5%#DEUFe|T13^)7OMh3YddI^Xn@22MC^ z7X|2p16k^c6ZvogOmI^83V-^;VJ|pYg=FlaL%o?w;e!MmUEmfbCJLZJuC!>VA=cst z%o}yw7!y&7e_6WV+;d4}0zyuvhPo)1z3;!|5_3>4Ks28#)YfX9S#ao~=bU+}-puD5 zNC)G@SZ%l(HXr@)hrPLxrDzE?D_!-7Imks^mohZ*6$PoTlRl|BKGdKs{yc+&x+^!z zQ@l|lOrnxoW@bM3inkR{-t>{z0KA@UopI7l6QhDfG*t3iE;3Z=Bh2e2T5%MIXhOv< zeVV(_s_QRa`ni2k7r2IoGjfLOQ|91CTRP)rL&StpghI{<4R5yyT@wcZ2Ul`2!B8s* z^->Z;%4eDiLf+Fyt}wZUbQWccD+A37mGnfSLM76T6CFgB9@Ha%=>~ze3LWqyEAz3! zKNlefhY2StOKp+wsAW6!@;Gm+kN=Mhc)SLL^*- zL=$uDGE;9Itm{k@j&b6Oo8(ThgLui`KE-Mzw2F_bI-OPUVFJ?(B+r^0SkIg>L$d6r zLpX@iDL`9jI=B9XA1#&e(IalFjC%atDwyYa72F^jx)b2kr-mJP6(o!8<-NxeYgvAoI1G#w3%XPPg72tD`H z;WiB-2 z3cb%#1{!twU!9jYaCovN{m!3tL`|-BA7o4*)Kf;C$2d5<&3mqOC34(!gnr~?96*vg z>6ED-cXL<%q_7GT=*$Yvp|e{~W`fB$@*qG><%6X{5Rd@EaaG(x3mz?qTR^wbf^eXs zBCB$ZL-?@*93)yPQ@?{x7dnsu>8}jw@l4N}YK5Svr&UcLI76l(p6W@gE3pC(Cy8-z z)g%jMMdQltV5<%xI45I>KUb$e5tUeJO(K1up$(!QT5Lm3XDVq;TR0HnoDOhsa79vy ze{KvDTrb2~MUyKa4)qk~TGIra6p*BcMu9X0Op`&*a1gf*zZL|;&p2!Vt2E9>lUsva zQe1JSJiSPe%a>))@@46B`Lay;C(k}AWp=(SU!HvOYxPne`|9`0UdEO$mn{3-1I#3R${mJCpax z3h^%ogjsGF1Wvay(9&~hsd%!SQ6!8OE2}ZM4`?`CMk1PpBViX~5Q-IA93+)mE9wf} zz+K@WQB;yeF{uQ#DG;G1>8jkE)kJ4F60Ne@2qpD&BNtqDj57{-2@cT^Fob{uLW+68 z60L^`9`(zY9WD8MkY~0+`dms7ZkWL5QXJ@3dM+CGy)W-}(iwlUFch)-{j%RI``FiI zmtHJcmJv&krN-*xGhYs!`vRECCx7!@-pBU6uZ!Puwm*~deQdv9_Dc4*uDfFLuTo$9 z^tZopmGk8lOYZRdWyNk-$k+OB0Wa3F`Bp%backBnkU=c?_F4NiCIu%j%_-YKMIFS} zqqU-r5ZtXTqo6weuHisHK4{$%*Z$SkQ&X~#KyEH$NFNYraHt2O)#e2vx)4wa?6CsD z7DV(VHh>1`%63$mFvki)VTlSeLEtFVP6u_~E5L9Sevpopf)8#;AOn`1x?)vK4#!#q zM**q8cZ&o!6-?rTbA@i;I)!K? z_`qaK_-`8$L1+kqobvSoo)w0VzJL!0rovH_rz#CGfz08q zgFjaFU{rOVI$DM|J^_)t4M=mUy7DB=A=mX!_4}T=N@-1S!J=@I=Ew%NDex(?n4L z$tVc=F&)4&A4wAR5v!Su9GYS?@_@^+sUPo1I=qulVU|kV6yLY>u85~A>R6>G_2_Jb z>nw_a%h!+?QO2hi7(ABoWdpm0^qJZ;q>r6dA-Pp<^|1@EJKe%R$%rrcHi>qy=$4WB)ZqL!KKMR z{L#~W@=HDDNi;YOQ4nbb2w8X2BPMlyZ4)Ue4*%;aQN^ImS`1die^O&qB1s?>>H zH7UFrIUodlu+?$uoUdplO98>Q)njxMI1+_Q%%}4tmwDpRvVIjMwQcz|4J$yj0x~KO ztArDwI-E>aN7yZhirauH@JIfoRd)ZCH}@Q0u@nNEUL+~ABdG><=c znuh@2ZR#arKn9SY+X4Nnea2pk$VsL~uH8}Wzl|Kj~kJX-)^B3T0syaJA=f(oWv znz&YQORli3Y)hIeZ@f!3~FDT8gDo^%#bX zFo!^xFr2pR7=%Ww#n8|-TpP)aYH*l_?l@dtqf)*sg9ev4moNM3*Y7{`{XhKSx zAAi^%G2hqw*j~o=uTtK}erVaPUdEne%2WTp{P42dRxZ8up`|x3z58o^zr5`3t3G<^ zSvzn20w2fr6XwI_ZsvV#-^X4!Yx1!##h*!?v|vyFB<264{EB%}j@(l@X`v5)?R>)g z9TXq?^8001f5g0K?v_&K;}6>U%n8n}GH(CByxux%Jzti%$;gz>g>6GlZ=G04{bX>x z`7DVOuE^TtiSXr>(nDx~c4V#KegrLFMj~MZ)Z`UqIj>9*M6M^HCPIdY-0lyEkQ5Ny zT=ADG;tvQ%LW)*6EsOX1qDSSu#wpsAw`vtyAS6hMReEBDc{wb*)FrTD5)cfl)6gv@ z9tg;kC900jSpiNuZ9!l%2e;5*;^r1YIzW)^f+#&$AdCJOs$hbU6U5u(zHS}<4dOOG}-TzOOXAL*{xTy zLDU^Uf8+yidH=g#d)6rnoiBgtWAFX=hcEV}FDJ{AW&bL5<>x0y%y-@LP5JVw`xpC4 zwxG)<*q&=uw~Px^Y@My=;MR6U#Wo6P%cxlD-3l)ui-Cm@?Zw~_uo`G%>rnwW4b8z2 z;_MnSpn5pwW`r@sTm?{p3@vW8g3!=jPYq2((2ZTx12@wl*xw~DhT`O@_*CdC&Qa$9 zx7flICy1lsqmEoF1JSw)!bJ#$;-pXsVI;1o2nAP$yFj3UU?uRtgnKFGiiGkwOfTUh zAqbi#kTk(T(L_}SrT`8KoQ_B92*y&|h$>u6F@%E^6EwL}2~XPQd6wyXlM}Co3vgp(2_xb;RJ1rAVwi_~7Qpj9!Q}F}+ib7Apnkh7HHk z5EBeVSrBX`5J+b%pj$v}iJ&lJv1Ni*;ix9Nk|rg;sUW8(bLdGBFM%)(6CGsi=~lN2 zIOmX3r-xkk;e&I`bGR!Jkdcgnp?)Wh36voVTUSglujAA~8VMnU!vsJ@MS+jGD?a%d zGPasj0AXTSZ+6wnfl4}{$|zUJsEB5@PPdSH$e@A*BBH?vl4zUg$M_O1 zyMugaz(_a-&oy(ueX) zgUOt1Ar5l|M5&&=L0m~01(V1zREVwu0abNZ^~zWZ3_Nwp69IvquBdRsGr20zrgy7b zeq(G-Xe3h!kWr+k9`ZcNnMmB~V!rNaNm0GJ>=@7)u_vAZJ1}+FCsr>JK&YY^Cq~gE zbbs!B+9H3qkR`$nm*vIY$M(jxjM(isaRz^yGo9vJqOD_g!>QkI5=XQmO$Nb`O6s(7 zYkU${iopa9%s@8sm8-r};UyIa+0I^Ti{7kjMo;Bit>7>r#M3~+L_L@CKS2;cjMX)m zXpK-R^s_`}Dh1;uB~@LeKg#r5sZjXS9S8ykBIotuWfF=6&(Kto z5_t?$1A=MKWIx2FDoj_2aIA9ObXnkTIR}%{)#;pvdMZhoE)m3S_`?Zq8G>g@hTuvN zHPR#!%2eE9m7dw2lhvnlgOe3FD_Tc>4X&UHVZ!jajll40h*~i$vn>}!so`yQv|y_L z{*Szm;mPgfdtb_UAG>_Hqi1z5bo7;MC(DxM-+Stjsjp-&e`3{A`La)B%a{G1RQa;k zu>F&ieEFee-<2#oVqSIsckjFFt4qIk&GN-x`S@>~>;I(6mk&Q+^FwBC;`hsk&EC3v z*t%C3IM{DzZe zz5T3%E-L z;BFDhm$kYDcP8skSfq;LC7E*RDpo=W+=K)bIV@U+;0BI~I((Qaa$9IB;4mRzP6VJP zX);6P`y;K^3P^dC3bsTOfeJZR@X@MZBE#hnO>pGG;0m5@<@JuD{0!MI(YVe5mNXm{ z5*#9lr)z{NPS5=fjTD;>-XCLye!=#pn{f(6G zecf~WO%9p`azcpqN4tZ4)}|nSXwL<=5hyB}0Il{-A`{J=ilp7sE+9^@JB*@L-L=xj zF@2{{#}HKk1_TuloE4Qph`Cnyk;J(uq%ywRPP2FH#R zB{Y~6S#;MlvgXA}JVgUf*6{~f#DGpMHar|a570-<`9QN4XY;~Lr1|Eg(Ok_URWx8D z!vCOoO{*n>XcWH{g`aygkTr$$t2Zf8{$O+Iwv}UvnjKD4R)i`UaFRkX{qPYnQYbhj ze#~V;Cc_Q&lq-*OoI(@yNgt=M>f;k1JKCo=y}#z^7-FT_GYn=GeSY zSTQ8#5NZJpW@o6Np^PC2&LD+MvxT!MG=1I%;sC@aK%^Wq5iv9bZ@#P;}eTd!{}osUbduG@W2i1fcdQ@Xj3V*}Gr@oZ zWp^W=l<$>h%l%5v3_(f-4KcBV8w_!hssSCKiUpg6^ z&zo4SgIGh%Ta02u^q+RMjn9jpD~Z?a+V+-T|7} zo!MU&0bvB4RwiW@plFBJ#V=6N@#EAr)KTb6qyz=MDhZPET3yf~D>yO7mMS>SDJO;K z{a7Y896Yh%^oIE5mwd_p;G(c@c@W`)cp4>A=rWW`7O?efsF)iXkck{H3y^hlY-lKC zu29g4ht7^pzZGBu^rR%BE~%!FC%Wn32_6d3=_xV|X>*Ub3F<=d1N`YkYyn%bPJGKx z*Tt~Ak`)V3oC}Dq2Ji}Go5($ktw2^CPlKIh#d06}vcIfM_Q@};e_{6WvgfgFD+9%{ z2eZ}7p2vRTk=vere6cNN4`$n6e)fs`zVn2any-2E{;U1}^7&^Ry4pLI@;tWv<(;>B zyZ>L_Zo{|iwAo7PWxK;(UiKf#>SdMkVSB!}dilWl8`)p>Jho!l_rB(C^NzqDF(0|d z#_DC?$NuO+vyMJ+`=j>TVZXVXzvXo=xWBA+t5CHUY%9~BF8Qd1(#$?-uL(;aA)XEP zppiH4w7vkT?z7uVR}f-5}Lq=9b5GF3F|s+ZdrhQ}j?;5=~vaobgm zBGRmSrL&5d9&R8~mBSPQA~9$&ux>_L{xgKLZ-t?xldDW)cKgPl#zzr1&g7VG(-(gg zH_jqJe6QW-dJB~*6}Mj={G*n3@h#W-u9w%7mBYYe*uARk(QAmV8rlA`f?CyVC;E~v zoS|esaPQf^_jUGZANT(9h3A~;6=o%~uYP%X`L0FR+p%vuZ2?w~hu7Z#qxkhkIJRCQy8 z;4vB$Ql7+*6Tq234ClW*Uphw{hv^|j2?pB66fmocV;dSqpfDE-K9`Xy_l7urIvr@X z06ZWp%a^+3K=3XZCEr zQ`y)uikVR{XB1~TWz?z_N=(Xc7kFq8YWf*+bj$(;h_LFuh>-%kRrr%7h2GTQloP~H zGdxvVJOj{a)}>h_O$`IY^aD5#jq=v)!izJCE@Ba&q=N#X1QEg+L|y)nB0?1e#ZUnZ z=V#M5N`&X!&VNJ6riYUNPlo73Cu=Sg>SmWz6c#avQf%nN%?=%BY}`vQA9VAr87(md zQYeW~)e#(_5V3+rVNT-VKZTT&g|qI!Vc9K>Bb7&Sbd}&CnYpf;-?|EFu%28rcFy5&6K1KTh5od)8-az`_-$^ZY#tGLlYrO zZL?^m+Ez;p@eg7=h&!$`A}=Em>yvloiVg zVb!sHWRm_x%B=NtrSz z)I#J6fG%hb86t{VJ0b$xF-0VW$54j@gF}D^L&pvUkDy^j(ZcaWNma*X(mwaxuURw^ zPKr!HD>6JE-4QfXLpS~A1t}D|0TC4Xr|E-;UL6-h;#$^;$2J5J9WTYB50xrBnO2eU zgnNUTo@rat%4y3bdRha$WsE;@s+Q0*RCPc}<=D^z;T|^1jgG_3PZcGFX~0I1!z%qt zF22a0*Xk;hsU_vvY@99KoYV%`T;;IQK$UJjj1`r2;>LJn zU8S*#xT3hfT5cb?!g&MYmrs68*UKv9HkTj1 zZ=ucQ-+$_XXP>;w_r9KbWa00B>z2=*eYo#^`SWG(FWX^--C&k+l@{J?@O z{TJ1o_pbihm%L!TEEQC~DGWV{txUFkY_1?4?=r)*bm_ScB*wd_E{VIq~2Ec z%9St2;=$~HSUim6p;lX>h^lNgsdCQ{fauyMRdRZAOkJr|tRhC26(~6kDI_a()u9+v z-+(Q+U0wW`@u*eRp>|_wCTK{liV{@eV#90{py*bOR-(c|4nXjrt6_OaC+nOM8K8?z zSBp~~f|F(xb}?{XtKc<9&co9twtiKXXs!Uc6U;2gfRL4M9rQCM2?_=w#l)+?k`OFi)cFDP)J@-?-kL~}LJ&dhh z{>+)jU2ygZO6KdY{NkNAU*-R#Zd-Vz?|nV>jr*)*DVf~E4Y!=rg(PtLxOJAJx?_&C z6CS6N%UwW=A7`hV?w}CqK5mpqsnD8T_uw}>;0E8ja2f0K^Zsc%8EoKwZ8l@_u!@c1-TZm?)Ma-p`hdJ6} zi-A`tlsg0+h&XA`bvw$Xd?<7rpn(+^F*w0;Ia70qE0_|+DnWTpZZHJ!QzF7dni0)B zY$#83v7xXM=pxe0tEQ8b(T+vRb<@+-@Dv#iFse1m!@)*XwoquM9}ll0!r8^oTLpzt z8#*~JGU&0Pz}d=^4mxciET)qZfr0}ff-HdSI_aE`_(hC4Khyb>RzvO=G$@lxgB=JO zXhU@4z)%<6Bcp{SPw4LlC_}6e;VltWvms$;MNvbFs<8&l1;1(LCqYAqF%X20xeHOA zXiePosVb@{fo?>C#M2P5(9w*6=QIFAv1v2SPecq9qOgvQHgr6E;BacuTZCGkgJdxS z8tB2}_XHw_03LXDF>nbF6Vx`%UieiK;=}-=V@N+GG_Z)y3J)C=dT<(|*{FC5I2?}_ zkGW(H=fEwk1nFr;DV&ajSmn8)F^U1OLb*|NI*TN`rcjM+J@kNqmB16`+%Mc!eb(M> zR1GLHh(_tLPJCCykI&YgB92{Zn*!hH@2If?=QcrKiS!7!Ta^ofmohLLMF)6DHE~NC zgD3&W809&Bsv3gmB9hWFhf~mWQzh?`Ye~bTj*>l@9$cCSN+6|$BPm6h4k_PEheD^- zmFDCT&?lLq0<1ZF<; z`-@}B6tZwUF;mD8KWRYeC~MJ~gCIuN1;i=A49tS^A((r*`HZvq z3K~6_AudX$EJTc^JX_7HY`^v3+NXMIPgyO1D80u}F=AC%Jl0Hf4Crt4#7CY6w(HiFr7ZmGSGC? zI6N0UlQK07{5K@FhQf`aV?&2mWR2oznm*1mOeaW)-RvOcsm#4hUu&CZa;0MUgLY%UP8h|@rSvnVY1b>SjIGyH|V_hr=Ib@>cC zoyCyR7NPEdj8coAPNq47$OKBb?(ahWr`*&sK@UoCCL+Rq=F>J!-qOhgo-{)e!CVk( zb)#c|YvZJ$;6Tuug&{;%avm||+z=c~&T8!1VtrbWD6K|#%ed99YZ=>C9;LBODk?bG+!PDwx+sf)?|G(^$U*CS>Ui-^7m!Ey=UhOv?xXI@7_n&_7 zyHDNy_(M1R{!_RA$q(-N;(5RKj^&nm?}l&i|I6NA?s;tQFDsV)pVa(q*6_`)J!Y-7 z=MHP{wc|RF-Dj=tFQw*g{qDVHuX*77_4l8#R| z_Y)s{-+}Wso44)SD=fS83q6mmOjF+W6))wOYEkX1)b&ATsiQj1XEv*pmtAI=6_#Iq z?KRfWwacus_Rq*lX@AY&d1HmD=aKxSf+BH?O*im4S&4zI(*5_@c+U2UsXP;1>` zF&lz&4>O7`5k64Jd8V~~C{V^SEg-rw8C@+N&g48nc8zM;VjWPcZHJmIbo?g8VdiWW zi!(%JO_j8qIX_Tfh=Lgq7ynjjp%iCEr7i9<-CBLbs< za_d1f4COEl=ng?T;RG=Z5!QjO7Be@DvWTBHbOLrIveyqhKNy4-c(jh=+Uq|B*Wv z$1u|gxWOwF{D>x^OSvCjtYJ?qsFb;pSB9K8phbe8pwOX|`E&0qJ$N6KdezMXlWB(?^YB>JoFoFF8M|wRf6ASowIoImIen= z7*U9bu*i_YsXG49Q6KQ5hZ{GZ|M?&P>Y!7C(^!1h7$fF64S_t| z8jyrb^;M=oCjVDI{fSCMt!jnz1?-lM*0m3VHA7HkZIO^Ix7QlNQ!Nm#E>;oSP5$Z6 zeyp{RtbB&pQ&u4BDw%C7+jNGjqpK0pNfj%V%nYTO+Mx9*2puB<1aOlT4Ri016(X}n z1Th!|j)f?Rm?#-?SS2$MBJmrwJ}E?L@egstlt2(iRPA*NP9g2g;wOa9B5XQNG!q#` zkg5l6s8Y)ierKWx7^((@$3vE?;@tG9)kP!%!H=SdbJYCE0EumcF8zpfs-iXBvs2XL_EI>O)9%uY$k}Pe1_;Uht3wr^WndtyrLEl>kQ={!MbC1q*5X#g~bq_ z9I!$Nn!5!VC!7y}6y@0Hq#tf5b7mKhmQI2^$$^dqDdI+jXqpGJE+sCZOgH6%lYVQU zC&P1(AxMtpe1{2fz!2vE>$#d~Lrgdm5mTZClsJ4Ap%rK)TZF;5!a&A?hg+gf`5*s@ zmsVVXohEJ>|F3o)_425oC!5R9dI0_de$L{f!>~B5h zo#n}!%inwEf$uyy`6FhV%RhMLA@%YTkKFjf@7(>A7&Hd-Bsb2o@!8<6ox7pww%f03W_m|aTK)L5S?|!X3RL7~#{6CbfQh(|4juqZ) zL)jlCcqH2=H{bBu$wu?5UiRXH_S<`>S=-LtdB-~z-E`4~=Uwx)%RY4E2iIPGl`~I0 z3Bz8y&AZ^-vo=^~O|rWF0Kz}rC<9fF9)X6aH&vHy=_;sI=!!=*qoUGig)^6=toRkh zh{|LYE)hi~PB;V+j~Z4nE2%J=5;ZSk#WFSu@%QvK5fwV+=ytk2%;PyWIMr};A}-}|z^eB;$$x%t{l zJ&f(2r2PM~e_?j1mv_0bx*K-oms&V4w6ZUENkj4kLoWVKpKhtTuK(@=I0?pJC}MOt z3I^Q(o;c-32cT2lEe4TY=_P;|>PF9sA#4CK5&js3=TD<>{=}9`F*oX(Km^^y>8I8x z3J(zr0t%cGDG)^0DC;y2H}Z;3Ll~M1W^}~CoMUN-Gb=++2!`Bd6oXJWC_F^Z1Y&@V z+X6_Np$$OlBFH2oMDLV+7~7L%2wWF4Exki|d=qS+CF zr@zS>mDSMTi5F+9*-?^dL&;Ju!z8E@p$9;85#3xE;vPY2nbSptPy%S9oCpSNr06Uu zGTPh``Jfw{u9*_-H0*eFl$js`j5>&jr0}DokYaa0oV8=_*my#UpW5!}?*4Ap(jn=q z7tmz6ZmCs3at3i6=?6}UlWn7Z3D+8RQ+C7cZr)8lB{(_50q!v>4gtKQV}gd7co!Ca zwhj?Z$J8*Jrc=)nh3Hb)93lRI)dzCF?Q{@KYjx&ZK(?H|X-O+=EprhyjOx&H2Ne$EQh7#QLb2e63}2gI{= z#if>|E}|g}P6~-Sx-LJ3T40SDDm&8cFd1zT7XPtOF(i^9g257*p_O16Nb_0@;s8uj zZe~SQ5WxkQIjH+!U-0^YxClU0r1?-%Lua%@aKNPqU;{ zE5eqs<;_rQIJKNHnv*SBe;rE%oiq3$MjazA$uLR%;xGQ(FKD)(B)U1Dc+;c}k*g0s za4#O>6mp&*24I?xeEq(x|N2)yr?zNNLZML_oOh-?G3Scmmf?~$^Kx!`rtPSr*i>y_ z)#|lkvH9;L6eT3iAi?%qm5o-i^=vUyf?@2&sxgzQ{QT1&{}H6qr&Fo330_TVh!W?N zE@qm6(XpkAAH`fu5Onw?;Xpm|u>rNcZ9XNJd4_!Z7kd}>gsMUqEY^;_5ry)x;L3k_EsMVsifPqy6pRDJZ zYF*27K6F6gp7iDniUSHTn7Day2{9=X!P%@4A>oLk5ujibF{fUO)9m^kKtPzVLUiG= zkrFhV#o6Hqn5MQc#)F?x%rWE)9u%?=6nInPMp8r=rQb{ePCBs}#mNdka}|*WqeLLA z7}aJd4~a(^;zqH#$WTHdS&%b^K%t{Nw=L1J{H#)|(Q>sK5h1{GZE+g4V!!o-KLL5> zCqGhRy9fC?qF-VqX{01VvQVj%jK%Nv_I+g6k1CDrCi|=90FLqp&pxVN_Q@|>%r=_g z0AlO;kH7c$cb~l9_r9Kfe9<3%_ue0T`!@e4^`=)(>Sd2)+h5-4z3=q?@|J7)MD|Ml ze0lCxt0|fHowI>gm=Bn{F~q~zwwP@$AF}%<>g5CGt*26+>@3e;=iuGf-+k7K`_EnV zumu|&b-ka)7))|>CM*RFo#C&irS z-vmKz+XL!S=z>!f&jcR8d8KjnG9`dGPzb|~!ueLAP0GYg7J`8)bO8C{=rHisPFfv|vJX+w~ajpRfo`wj5ZUoc#NO#kWpP+LS9djas zhY!+J05KXH>)7BR{Sc>f4rIMicx45J!v}by_~EoQ(>$lp)c}3y8ElAfqZ!Jd?4}Sy z{$vX=3RMI0e9&2MI#IL$GiT6gV@rxVrjKIOkQPxI?-&tbD|!gFh`Q4v1Zf`8BO*R2 zY5FJ-IuWr!S`q{)OclGsBA%|LC`E=v#GKIr-2ua~qKcW63v$v@gp|VWVI+|N6lRe*rKgzDxGM+DFTKlq=8o$beW(TgD%rc z0Of#4c2k>C?MQ?qA#U6Q1WqavQQK|03_+m>5CKGj;TDV^U;{&tHa}wcb;a>=;P}AR z+)x3TnpKU+CAbyEf3py3t=X9f<{UOb4A{_{I6)LSy!uz_B{9>&D-d*LXaQWAfKx3D z;+LLPaY)ii^JIFXMxcraPPX!mEf#qg=$eb8RxaTv25fgr4&C6$?K#dxA^lD2%KK2pEwZQ9|K>h=EoGw)E8E z9CU`PCG^CT(l%c+h3Mh|g9ifA*3fj$HWF0X8qgha2)8g=Z(6BZIi7^AQLP20HL8`u z$?+^3>xWVPc<@6d`uBhP3%^#mM|&Ll|NQU&K(yVgYPO=_;$ISI9a5rnRRvq0Or!8; zq>Ckrd~g(u`fuOA`|DpwEEQ8$wHle=fByTw(54XP34a_r;?$ZQI%t}RrNs1`0!~AS zx-?T&5_|9B+p|SmNZRD*{ckl7Eyd}w7mi|~TEiBnl0?N3Q44SNVhD8ONy;K01x{y# z7H1_)g?2b1=1z(zl|LxX1rXx&ab6hW49(~{mN`T-YUPWJ2m=8@nJo30_-n(3(WpRa z#b+#};-)o(2#O~*NbTU)9fyFzfW-kA=+QMqXYM&J)OL(ehN~!W03nzx!47LW2Lql7 zJhu(ksiAdVR8YX3lFmWrQKuUuwt*1GZ*-iPkfFh7e)56!yn^s&I4LH@rwM8s(k5jV zlxQo6Am|+<$Zu!ST&7ygG++an#yQdKLr*h5(KzA`eai_ESQXZ!g=857tw{wkzz_UN z^W&fVaSIQRMMb3Lq>J7$>M-Rb>+E_y!Nt>=;`D?#t(rwsXkkPlH9ar$EksSbRc^p53T zwce^P-E^JT%-ZZNJ8!kpj+?Hq&3eo4wAs6T9NYK4JdZtZ>s5E#ZVmq`RlRI~dGDRq z-fx%n_L;MmSD5{olwuk9Bj!DKeCK|mI)EX6ohr=Wn#>x^I2+YhSQlR(Mt& zDUxAIDj$xjI216*dSAXP7OyUkn_RWqnWHL5hGL2aux@7ZaUI3iA_Q9Qcl{Y0-_ z`*Jm-Y;nwx-wNol)y{ccCen#W8*>12UU3!9P}a>$vCKn$q8Q}~i=IW-RZ5Rq8_}q( zWaY3OWP8V#U80Wuj16U5$;TdZ)F~%^T(PWzKKslQ@E~eEbPcMS{qwSKVr$jT9>cb? zY^XPv8#4OE3s3g`@?P_|{`k@RpK;QMzx27&)yrSG=&aA5cgi)FfByCxzjnt>SKfK^ zH9n5*d2DYM$dpR^yOO)$OH@kI;N^r6Nh7xEsS-%U?gCD+(m5^JkzvB4Xr~#Zql`NH zh-YX>Wm=1&6podYkb>YJA~~QNkCrF8iN`s_bD+`8QJ5#$;tZZ)7bglxZQiEMkZ$_5 z5y=9YL$t$mR`LyfIzIS|Ud{}0Je3>iL5~3)B`4_zl5NMuXz4v9C0c+#W(ekdz(aZb zG1CdbiHKA2B4t&XDn&cu1VhX>mkJ1}qG9nsa)So=u%01Ag3&|7mXh?q@$(RplX*+6 z$`QXga!*;If>1}0vg6zinrdrAfGnIXo|pxE7)>n)GGWw*wOs_<4p>1n#OMITToMF8 z97xeAq~D|{FI)uqlk=poVGu60A?S#twwTb6Go)D8qQq8w%*af6NSj?Y%y67Wn~s)s zCd35K^8#DwZhYy~nj<%)0c8T(^4PKzcWt==N(}Jma)#0d3=p??2~5nAjd-+W#TYnN zWcX%MLctT@hO6sn_Co|5G!e4{_;ouv6F>i}c*hh`NgF7KtBBRhK=vhDD@oIq#YxW$ zVUzRA^mNPXv~{tS@~H)=#VlyN<&T5Q4Z-JCYgku29T9WEma8WIykNGfF#Gb-Gx?sl zL1apv_V)<44?b_Q;X3XJEoO(CPV3pT#NK%F_Yi)}VS>ecut+l3am-1M8ASmQ-QsK% zrLaSEty{qP)<8PZQQ!`QwbXLL&z2z*#|(6%AXt0b^=S5_t&)0#Dal#nuxt6u)1-RAA=zooqV<&iLVTR-W$ zO$#h5nd5`{mlbigL;*vbp}Z`S327u}{^{@k*3of9;MTlRtNM@w_WkU6=fH_abS|V} z+Eoe>=~Q0>T@H`}q?|Y?fFU}yj3OFki%W@5&XlLViLO?y?r7;&O`uqf)+A!1=s1-j z)-ZYiZvo?}!o{CLbQCKePtZ`63vE<|r=LQ*(%*RWL4uuMDR-LaQbhy>PN%LjD2JPo z$eW-jHOHbVEJn?-ONwrB!zm^f5^%7{5(Mbet4oAai{8qK86C5s0Vn~w1%RI|c9}~S zCyUw+4Q6xzK@6un3MJTvMMOM9eEN7oT4|jZT@yNMI#aaZc~2tz=$*Di5_d9A zbUyb0a85Sm{K?NH{4vxlwD1u|f`VD)HHJ*&g}_23LW7{CgeRa?XSF!uR*2^fAWGor zb!*ME7%UQqbp_y-mU_7d8$6<5s6~a}dec%{v>CN#v`qogx#I{TY5{sjfs?;=Y^J(5 z)M-BEB|dlt+n@^qwJCI-c8*gUKQ`8>ssfEO&2X{|X+;SGgq~y@B@Ss8kRpk#gN1^* zt}8}YCkl=F!kD#chz4C#>lDx>!U|*>a0b`{qu4svI6w(HKeceCLHg5!I31r>vbEQm z)OF#714yIR_E_F3VwJKzW&6wOWyP|#N_mN5*)KGTV^82)cARf{`q7(wA6vcb``8<<@rupYd)*G3FF$v?mEZT?l{}B_ z{bf&L@4w3?KKbQ!W?ul?b=%c__-nyVYgaG(-q)NhR|LxBL-*R~z+Kn%RJLOIfO)GO zwA$m%n zKNz;fTv7b87ftq&@4WS9|3vk<^Ut-*{LU5LX6RKff4S$e&pGonKdAc7m7QFhwl90} ziw(KEsVKdDOrero?d)5Z=onO;9N>mgb*1fH)hi-r6uGKSqa08&WV0i-C1RRI$F<#_M@d+|3s!9#*hbk5euVZo#^=wPj3DwMz-WuZxXB z;wV+eG@vH}0c=slVJ$poIB8zQ)yoRui!ZXbZj;*Y0LpV;EI(JV{OQv_g>v3Gr+nt@ zlkGlV@l_ks=c|a7#$HcWGAoL0B5Q3Zdp5gzS?yc}4e{9amp^~nMdzQmVApLvdepur z9ec#*K6Bzl=bvhS*;Cp6Qp)?wzW3$-q}0p)g4zDEG^q@>q)5^wDV3c%Q-F(kS9RH! zw5kFD13)p_IfZAM4QF{WZ37t@U8!%%2az_+U|2LXMu`C3MzP_q8yo9}0u-ZB@+xss zy6|iz2san=GQ@46P%5^&m&In99?VURs>DZ&5~LrU2uOr}z9Hrb;-Dik;V7sw5aWT% z*x4F7@nqU96%RUvT7uOerS#b14AG{!xe#Om=crk9Gm$^|bAXg0(R-INc{uSx03f|A!A{b?^!m3Si&~4$Fex>bW)c~7KyoYKq&kh zb?JwXVfqIykHGne{VMNXQJ|y~LP|dk+2xq{`UHd3DL~RI8EP@4*{E&`jY6_JQU1(RX3>%`{Z4@{CLIEJR~17J zO+gbbB4aIyMifCq7cp$017|eAgCcIlF!4+`WVARt=^SMiQ(K!_vV%DAR1M`2lxh8W zNYIJXg~(avQec%jXD!;U30)-&buvP%;Z8K?j&3l0GGaiiV4)|Y&DYe%?5ccq`4GfY zlu*X((1np6BBO3HijFwMmp}}Oh65yM4%tv^ZCEW1v?b({YbC&y!2Qf(c?6aXiuE}t z77QW^IuUCLg(?iKCAiUCVy+X74*;ISO+U{~s^vihg$Qp!%!wnWfq1aQG$|xbk2Oga zbCeWz!or2FU&C-(#?D_Yh50i))I~2=S~v~EpQgY8Lx`l{7}({5JE{AEtBoNSF-B<% zuwhOcPDB*i5Q8+w45wkZfk-5vGeJC%U>J(WlIz0Wn$(SAO+qYPbSvEQ?jGbRYjtsz zvd!i3_fq}CRF7oaU+$yXnC&S84`8>mJoAmO`)A%2s^dKHTuoOQtC~Y`2j6%zSYr3eBkx3dF3*%eudkX zhprc1dzE7Ort7ac<|9X|pb@v(ax-r<`!BQ0zH+fIXn7pkgU~(&X2>_J)RjJY&5%67 zZ^izL%KKRgNrk8VUql<(%1;G5igHycJyTaso^#gCL;N^ZxcoE)4Aknv)uCE=+Ti@h ztk%Xx1AqKnk7&P}OIvNRvGf3gvR7M)E%yduLWEsPbjc3D38&K#1_;M^r9d_WAcjy> z%7%p0WFhvZn-_72C$W{x-dt8DLsY~{<~E&sPTP?3`I;-gJlS0S;(67}-dR>4+e%gx zE0$Htwvs)Vt%&wsa}~5v)wE*y3m2Spz=9o*`OpE!ee6)5$o|}CPWJw?&E=b~yTtyo zN3wk!`-z9G$@`VeLSd^;9xE>`cY@m&bF#RS%ht-f%3~3+jYK!1+ee5r3_?jFwfG^3 zhNc;GV;)1@S!$i9sWm#F`;-=iPU7(C+>aFTz>scw_)wi- z1xrKB83in&jOjE58m6Jqydpw&=!bIy-~p-zai#>N!T~?8q6jjcOr#+}Jc;KICDL8Y zlxGVYB88wDggAf9efWb1po6r9X95L6)=B{4&=KbhB~(FDWr!7B3j>7|PKXj9PA<_# zIV36k+ygiVeL8@|$#(1@uF_-$D!Vi@>H`R}=st_&^+wtpI8Q;+*}6?bKCqjtmU2M3 zy8zAZ0({8rT;eS@f{ezgi14EUq9Ht~YF^ryDWnIUT0X#yg65cAo)iQOMJx()xynQc zmrU7DH|Awh*r6O8=c(nf-`fBl2yTZ2-7m)Pnub= zlX8PbYp0# zP#9D_IeaaoRB0Vqrd_EVUq)LVqa_9z=MH%{pndP=XUsXu%^o zSS-3iY8lP94ioDr;mtJMOo1V?ju+fyhr@WL*?fq=*(V4lh>k5NR@0oRMF-p<;xwlkF8#Q{K1#CRcpSK3ekq2(w{<8mHexZ7~(%9#=Y}BeWRfslTRe9>%%{N)! z!_tTz9>r~>6MXXr$f zU?@VXSk<}Jp++H8RTPt=T=hzWT9rc9mB#WACHyO1`cf~5c_I#j`WiDHt;wR8ctvqV zI+q5p&Ij76qcOlGHo62kz#l~JahMWBLnt7g3TGwowO4(mlG*dvmtX3wXZ8Mt=bwA3 z&E=aHdaIfbL@qez(m)?5Ar);*ow>;L6)Juy$fE%JJSxN?jq4-hofQH6xrSvT8Mw2y4DjsdJTISFv zcbS=NmWQ3-x`uQi_?r}MIJ0OJT}w%GBuJGwsFc;qenU{O7{ZUU%Sc`&o7+S{6l@?$ zL~Z&x0N2uAc;WQ31py3Upr0Xb^RtPk0Rt&5XGET$)(~^_1A=gFU=VYo3G&r8|68y zH631M4zm-@Cj~v9>BS1aQ`v4-&)SbuFVjg+UZnw!m_Io%>`uGay!byrs^c|Dhrf6tj!AuI|@hx2iU3=mS&Ny zlw)I#OLfg1UYsY9nfZO&O~1^xJXSZlZuOmt7>-_PY*gCs<}MMH@Z)jEH-w?wSSy#9 zh&*>jvj`U@%SkQF2i7|TyycsD`EAN#j;K;|I+(TDnHO<%QZ(R1*8+)FQ1)J}A;TX9 zFG@rdg)kiF63L!!TOks*n6-$L;$mhi-?88%>zskO?rDX(Fp4k=a2(6N%9I2_K#Btj z90(nXj7!AevJUqY|2^^6H^0H{Mjm3C6gs}H!(P$#Uy9xg5;4CyD`1OTrVG^Yp%vcR z5Kr9>eT%Kp;WiC825#Sn9kRcl$UMjHtoJ2SpCYkS zZ+|%}twXj(&dD^F00xlH3`w5x*w#_fzTzcIpLh0|{)5$vW(+O5@p`Ww^8qskoaO>2 z-W=1JHc79uz+t6RnyNLb#h)$hzx~rc^eHe?#%vc``E1csi!;ZnShZuJT7+tl+UjLh ziN)FaT%umaS;5jt5t~&FXaE%^ogHbQ4P9wNuu*O1Q1JNa$=CBBY@u+>FI4&IRKlqU z0vM=u%6G)gR|H!B4&_(|(NTcWW?c*gL1fD0L#z{sQ2{qvZXa1vy|B0jEL;?6?NXl8`)cEbWF!K{M+<7;-6wF-KoY8RC4S?Qx z7eiMJI1#6nA!l@_GYWBC{^TK}IRG&!8iA++VH|PBpOacRxA9w?wN@sa=ek;5%AXgE z($H~%;Kw%6_S{1VmBw1@!7r1ov6dO)sF(4#>Z}~A7lXyq#~8IP$VTy7PAw|;6h9*M zGOwv|!(?;V;X~<=wIphrGP*9{=cMj?ZeGnaJwufl;U`##;D&G(v=r(hI`Ks;Lq_ui zKW94MwfONAWFBJ136jEzp%yVeM;DJ@(kSuuCw0@;@4br&*CM8ex8{-{*)gw>4AnMM zbZmeg#}_~b!?eycED;|!i|!OwAl%YzeOjv_M&TZ%05fabLiMZiWQ*A)FuYflYdwQ) zs10c4v%2_qOJ)>wm9m$YJ&&yo`R2oS{^$o!cpm%7M;3kKfoq?6?B?%2b+i9pUi&>S z*>=aW;h-DPKO{MMZ|Tgj)bZ7%Qf-c`NDeDJQD9W;MaC3E%i0rNJTv*kN? z+kVwOcU;Z?FZX3^^|BiIkloihaQ^E1?y{QN`Jg>ES}C71rI(Tw(5B0fvQd%Fv~6ouq#9GH%gQKG5v35S zR~4?RlUD!_56Q}nrOMTwi6~czsK@0PI7z8VAvh_4sF96&2TB*wlh{7r?SXR+G|GDQ zGAC8s)x1nApH;qqX%3(WOC>PDtka3e9|%E2%qEB#5k#cQkP@DtMCkCt_EQeqWwyVp zSnl&*is%bIbDFBz>&&10_>n&LrFK?9tC#y4cIB|WW)N z1stL+Ju`%7iK}|WLUfmXqv7rX-3!P{#^k;38Z>lYz!@|Cfsdhbv(ZvW14Ec|Ealx+ z(n$$%7MMr z^v8h6{{cARe2|NZNk&zmZRp5OQPM^^B|OKUW=Q=yL>E8+B^Y=pb>|fghBAu6G>Vp$;yfmc%@ElR83YeO3gdwzayx}a@dF4-v*tkF zrd->kNRicrI11_r-xl_FJpL5kF;ZeM>ZozMwr$x|85%}&K+9sbjB+o?7AY>_F@1Y{ z((;y*dffMlStkqf9~%vs7!ygespUXZb3h#rTpB0>m zFd<6V_-xjhjz4qSk!zJwmc+X0yP<0l-O7~=TIFz~hTsyLZ#;Ovsu>T4AU2fpVQroi zOs09L5Oy{-5n*wdM2y=a>=kmSK*v|HtX_lT#4~aAxpG@KpiORH+Ar?{aQU0;3%wz& ztRAs?Iij>pk`|ECmGH~x!jt`NJF_O4F0dvdq@DYw)}nPdTv$}TKoToHvY?VB;T0>v z5hw_v)J1&kcA@CfW)?o9q4aal^)>+tg$|ihj3M_34<2{R@NyH2Ei*_5nj3s;F%aY- zfQ&=mki`&0=jwF2PK34&kOR^A&aoD=Xb-hC4?R>_B&J;2X=?H`xOthliM3`h6pfO} zvOaAIzo9ZWlu1BlGJIJa-1^~++5P74e&z1@`q#d~7Bk=e?VtZ%pez+PaX-8Zj=B8v zZ~oTOVz(7bwxpClfazRi6c`F@@RG!Jjy*==`Y*rvI~lmLT4bC)(qk)uo|Kror8vN1 zwsb9KOLw|5wQliPtJSmEKorYZRid!1A|};nttE}_T<9DLCrGNjW14YhR1xIk^NQt% z{ru>1>KZK?j20q(q=ozz^F zA7 z`Rjx-q^vONR07Ty=Mm0sBF}pQTJ2pki95$xXC((9qQM8NQB;Mo$Y|kWF380#KjF#Z zN3r}MCY!dp=+6uBm}zWjpm1C#9c#O0H*hRi5;m4rcF`D)Vg+(n+cHt&M zC!9a*a^Ut`ZoK)1YZ0+E@m!)OXckE^RM${CAyIUv^>f}3od7bu04M=Nu=tcVTcXyg zb!&-QYq}xbJK()~;5SS^A}W?A4_bRS`G@X4@;7J1YJt>)YT}!>Z z-~9D@Mo-AIY(dF(4MyJW%c^Zn11B3jpt6k;H@xnZs&aXe+aE)!=C+9y=89RRWqZVIDJR_uyBjJtF}p276rtgq)Wr{!)#!?4 zEm`^%ycpnys8S|trPj(-&OmH=_dND4JI`W;X;UW7AA%M2Dr-6osgVI(LCq+&j8cdK z3{i_tL!L87inhGsxgcoGng(H1S&M^Gef@P-KK+zW_K#8am%XIyxoc&z&wnYRef4X* ztvCL?BMw6MA~W!}%eI(3YYh~|>Sc(!*uO1bd-=sSn*FiVS*ILpbJ-rWFJ}8bwvyTV z%g24}5Sz=skF8$5@SGFBcF6^|F1+miI~U$}#|`(~c7saUBia6c*#*B11s8wmldhDh zl&tiu#3_pFxwFYl!9~6V7!L60;&Faqkg&nioZ4|y7}`d@hm)ZJLm^x4INgt&$lZ77 zcP?XQE}d{hXY@G1F-s2d^TgG_C?BXILe}ic3A>|Br{X9=A-8XTS(g$HFx2&AOg!uP z%ztc#@C5KYGzFY+JP@`j<#IrQ;2B6?z-yN+B6B#?kY?7qFVhCkhY$>;sKqcW!T}|T zQ>cqgA}YP)01DwYer%XUfKmLq5DE$E(w1^oV&DTh90TCq=w<>ZZBpMM zqDx9k8$qV&B&g+sgNTx2&4nJ~HhKNX>3e-|d&{z(5Ak#>x}!{kPYQdpLsDH!vx%5B zpn*RS#}|=i`W-Ch3~`teMq9c@4dJBUQ=o5r{j2@x=h7h{RX0ZQ=PkPw0!mP{C{3*h zG&_c(^PwrzMv5LoZ0X|H#Rll%PZb$NmlXX1LJC9PqLX4GB2_4{iLEYv%oxz&+7efZ z*2r?*ZQYc$ZvBW=9}Aan_Q6tFjZv9fX#)g5+^8}EKr9t=~c0t7K5dA2TtGec83LP@7E^!B|hpuJvY6rQQbWZ9nfiHhUuZ}lY_Z4_hr>FtDojMeAm#-V4~p*O&^-47c*&ct5DA4OP&6bhQWTMt zZmHD7nTG%IZ~y3yrWic_*pGRJ!FpqkPGDcvt=Xz`fAJENdfBYSLAb=k;cMYh!ZFrc ziir6$hyvb9puB9y()H}O6=gd315#8mp-nkDH_Y8p3sR1gDljS|^OdTVyqRX%ySbZe zF|ll{5UtUIgU5nF(M1#uAU<+DNvZS?k)Is!lEs2O=6M6m)mX9YBI&F*aQVz4Oi;V++|2Mz%pq;$v;QjZx^`;w~Sw<;z`aE#YV!u1VNr}dU zf96GdwT6{!HL4&i!p9u$nUcA6+oHvQO}POY#gC{+@!cea3A*|P(aLX0V?*rMBrUuu z*ogzFi%!bvKy8}&;CvwkFlU9Kt^j6aI>O?}ingxB-%2n?Ll#DB zW2F6#0_dno-HU4Lq$^Sj13zS%4j1HXg{LP~tQ!hPho`xPir-L`!dNPZII;1N=QQY! z;w4*M9t}A)oC^-M^TKKC+GPAmog3dl& z%B?XzbAVkg0SIR}ke}04Qp_0QFD53aN*XgQfOw+gd|tp0u;F(;vZ(9WS&dc^upX=m zFu6#6@7pL=PK(E4>uPKE`Pr5`V;VM2|fL=)r7W z)`<(ImPK~?X(FZ|+Ey2rnhs7lo+g4*mxtIuN4W?z5u?cxHzNQ^KYC#tEuDphENxaS zb4Kyg$qip^a{dZ9PC!FeIDi2KGa@{Q9vfA!)|UjHBwQu zaC(d;0_T+xy(bm$nm|fc~PTlTz0~vP^+XpkBVf5b|a+rq{ z$bBMPf$WRfpr^9OE6mDjrS$aQNGY7{Ecd;y>g6_HbeKwjrp1te(>g5gAc+>LB zy!3^h$M*BJa@fXkTg>W2TcWBwW!-yLTEUmGm4UuLsSI>a_jB&2Pdiz$Z0~qQU)_G? z%hp`=J${Bg{p1ra{``eoZN7>Bt9sj;-)IBcr@K_mp4|34wj$B(UTN$*pB|gE(X3Wg zv9e_|SG}oR#lQ_IfpV206{wnKuavZicye3(6JYj-hQy=*CU^V5)8m?7p2Au{dtZC#Gh=@$mh8xk>svS=nu zp?Y6Y3OB(?W$ z+kWWq1!tc85q~M=V_%ni;p{~>e0A}ntM0k&y1N!#Wj>8p;Q<%w>y- zloTK4bcM?yD~y65AC?u1k4T!JB0`|hl>>NO$(^vS7@phYA?0E&2ynnLs;CuDKSvOB z>fL?kB6^If(OmPZ*Rm+Ram0B`v!C6l@(w9xuSRmf%aT5U=>L8EJD%62Ef_9QZo#lv zwE0gJFeDc;!R9M0=18qB=bQczVG-2DkVsJ?LR*pNGjlxA7I_p>_#xtkj*}0%7ytxN zg=lsPjbyop)BU#;SCU_r(w$xMhi=r(7eGn_+>YIRlad1~6Vw{_+<6;W{AB_uOas&G z0=9CDOJZWOToN>%^G8<<9UVtRE3kg(=yxiyGAaVt%rqsW@Ea<(uMm*WL#mXupcF2O zWK1GgbqoR0JYB^?b%)HqtWOK7AXsFj(~4=HgC^x@T7iIXh`&8dGf%A-G!c`Bh>6%% zBcQ{33fCy2qu@6mKb-p0M)%p7U+jO#kl@g7`F-%v1AX7aH!!VU_qEn-7IVNs7azf~ zZfSF}=nAqGTfOX{i@)m9qZb%l*xl^B24z$GU;oK}DE+KeXHukEndaiO;*1utbtvXI zv9Xxu$$Fm2T1`{lRqADVxFvWC*iQr=*VpyqfSIZ{b!BKRD$5L;n$&8Mr|Gs}Fi2%3 zmSTm0C(cmE+8HMfBI72m%O$}vHSV=M!~~HNl`mNu6gyN|qPjMN;8w3i%Ugk97v0Vd zA0`Ll558v?SOT-*x@&pY%tipuDbezrlCn)ljjJYN;wEC$Guu>wzxl;q(!&HP{H$;Y z-M_ZaNm!VqGoQa>^Xrq}{VF&6=s2*F1bKwZIs`?<5%LPb*aP(;s>5^aK_tt%{! z7gcz~JbDOO%oKKT5u+2wPh4P))}_456rF}TQVtVHIfgVi7MUh!lmqGxEjF?dwQ%P` z;LNfhoi9!el&%J@{}HtqbZardaiVJ@TEG#VR!(Q0GW2ofIGlTd`Jh;C=zDXUnoBYFP%B@t^&vf*O6YxjflEp84x2l`=d~ zJ{xV{x$k|ox$K{p`%tzQnR|b^4P`sa>S&|hZiYX2*A34;ao_izzW3q#u73RM*Z%O? zJ1+m?CpK99mFi_%%WFX8*su?fP%sVdHoD^W_8PZmMdYyVWXSk6CLenfKgr z&H3A|vd^?B!1FRN8mvdVC&CI)XI`S&Q*t4f?^d$?*(=`Q`M zRHdcLR2?dL@G(qXrL-;wjM7R^{z#JT(i)-%UcId7#-mF=qv})juk;`vbQR$0U#b}O zr7!;AuP$cD#QDP(1e*$1fy@V0wkU}ei?k`34YA8rqts%cM9Un7renXJ23@u&#EgLn zl@~S{yA5|c_%@NRyZVyTPX2^#WgqoYFIO6C70z#Y)2laFcNK3jfA;({>^@(3-s$I_ ze*CGQ`sk-m{-hmbrLnJjdHULW$_nRRZH9O@Tcr#X&R@Cc^fOL6`oO(sfB1;KPd@HQ z;QypNkF8|(3iI8!UTcfl$FX}D+l$On5h<&zNoL{faM5;+l#`SYI%^PIl;z zn5@ykIs7qYS*W@24Q}p+AX>8M>M7Z~bd;@WNHfp*nODtSJA5`8v591ztBCYZ61AEU}01*!<9uCMmEbF97VLF=O?%Gs_+(&!ll(sDnUv>MEo@1;Vt3_(ezx%Vs41r zDJ12hYDN%%ze8`bcmkY53WXjfia$gbkk#gY@yV9MXO7Z!B`_XGgb4pBWEbw}sQYMh z;9SfpAt-sqENGk&UPLVs+DNhD+;{SdFoguMLFlpIn}P%FP|}Zw0~AWmo#|?jRs|)l zflg$XN|z4WSm(xwBw5`YptFb-3ParE3Wex*q z2!&d9b*G_2Ko18nXbYgMRmvpGRZsxcoRq1grQ5w$SL!8!aktfl=wcInBFZ9r&9VrT zw4nn$6XxgYWk!R1)AHXOTLuv4monKQu<~6b{aE3oa5SU9i90&2G1DaCz*Q7i+l~z~ z#A;_1M74u@Ax>Fb=^P5swI!!&paE1VS1j9Kn{0VJchU*Cc(M*eoBx8zpImY@5htTF zn+tLkIZwHepb3t^Dv?S@+g!$J$k8`Juj2Z$hr4jwsdRP`cA$5kKgaV7eq|LC^)kAM zvCc8AGYT;;$(S`D0aH$j21IMMXyXwa%flkkmDCX_NiKWT zH3XFNUQBj}<+*>Q6bBPGN1j_S=F4pp6hmjI=vmC%@c9%D_?IZS4P2TPtu1e!=*o4u z&5Cr91DI92q*zfbdmj6JvuCNhNm8VR2XmRwybcwBn;H?1n_rG<87>M%O_Bh06+YXy75jYX~Pm6ewK` z$r^P8J3uJxGU0%x3WXKjG?2{`U4nofkO_i3j56qaNV$WEsGC;`jk;*h2!?)LA)=sG zBU_Ma?K`A8ci(>XHCK?*wbF3%fn%9tSCGx9xi=j{=q8R080t7e8iEkP(>f%Df=LY4#vf;%k1{!AtZhrt%0#rtWe#Jh|MVwb&!1Gs zKJcZri44@vJ%(MYUbg!@_LuuawvFcgTT1!dE_2Ud+f!CSM{Ixj`%gdk{b&5))OC+N zaLqH1-{Ada-}{=g?K^z$YwHb{-FD;Ux8G>_>SZr4tC#%+v&XahIJW&||0lKktko6E zd(B?c%gb}OUTK%DSKMvZyKOGpQ}(?t_40z9SKfD*H9e2L+YW2^|K)eT^%XBvFQ0nS zaVlkCXI4?E`cvyE>y&uD_vQQ87^2G?5l=hmc(t(KNDth1f$RTrZ+IPwTYw_^!$%$N zsj}MH_OYs5&5oWw*jUt>4hP^oF{FsbY=Tm86ykmj z@O1hSAK3rao3Fm>j)gXnJ%;U@*!GdnJncA$4}WoQ_xU?6zWoLt`|=92mzr%CdpV_S%GxL zwOw*0VUs&Kxw_grxkhd*zXC*Pz(%A@FCq#$3e7Yy*PQ}RGi^~glO6~LL=Z8GmJ-Sd zQUx&NSo)3PY19fpmH+TFD@v|Eoi!`HX z;Z3l~PW#Lv{V0YSrItCnY23&GK0sJ3KKR*65HsLBB8sFDTevQT+MLvyFG2j+fEmH* z#Mbo5!ehfZ4IKzG9U?Rj;HtFTKDgYKS`Gl5G_ac|!x=>85QU}4VCDuO3ye0DNQaL$ zRs>T%FZuWEB`GPf;8aZTPhTdd!Kt(_v;^7XyD#=y8vh(8>X1lI2L!Cz(SD zhP%1@et9MaNj`um2b9=~1?PztkOdNnh#07K3m*QM<23NikXdkvV?1}P9VsW8WH2qCa@n z)LiF1YjpIHhk1^|d`(a*0s`VYnLe`N2IL`RoLv?~29LOLk0)X%dT>`@CrEE47!@vM zGAXw(|J(SAmrlMctpN9EgpZT>^J01|=GLS`09@)@y25ML%^mKzVAgG>W4@9uv$G^7 zbD7z(vUp@`=Bv829t6Y?D4%Ra>6W%(77zs{N}SjXNz}zh07cu6W*){C1fMBzA9uoz z`+_M0>)&Z8_|^k$|NhUv5#C!CURNG#M}|>OdJ`F;P#~#PMR!L{KP{8Ta(GE<=p{io_VnC+v}gY5nG?_7ps(8z!0n5TD4*= zY|JPj?LIp-R4aH0s&gqx1D%~IiD#wrh!w?ZB;10SLUbz8xzF53vK3#`LiD8wZbh+rdp*tupCfz0s zAzHxBJJ%36!A2+R{J9tH`QN;vnC$Zc&pB*HTrGO|&C4mqZ8Hjt7F7Z1RCTrjs}xw4 zwotW>n#j;50$L(2iT0Fj5UYzJy4Hj1s)D!uVvEH3X)#z-s$fO2&1lT(YrD*VxQg5F zFoymwVfO+4S#jO>A0Z*S0E1{tS=eQlvWQ-mg@t8d=|zAj5}l=sCVCOQ_bRFcq9b~5 z0#-DkV#h_cBwLA-$BzI1#CB}Qa%}(O{GHeL_ZBz5P?G1F=RR|1&YU^t+$rZXGxuIi z%K--4);QopRCSR&3F$&3l8f{pl7ukH(-WbTNLeBoAw#(pvX$pmB1=k1uQ+R0<298f z9sLl&L|k!Xg%%T3O+2~itCtv6SJ?=#N)lw!5%fT#sd&%VRiYF{l*LmJ891b7)zuEG zcPKItqY)G&VH?Wgn5zQKc(TE&+TnnOhgPjk8eNFc~6-m%-#-TZW)T ziVu+47V`*7|Mm~;9fQp6FS`YeZ6$-oj$yaa3_c6aW4i^GfBNprKYr)gd9OWafBE|_ z-g(hk2TWOczVPzQjh3FZ>59Jhwao@A+Fyp3y~4cbtZ6&Xm~4O9_pyEQ%lEP2%SIE;CozZQIFvZnyT98?QQP#f3i&F9#|iVwhAM z&=bH+!S*Xa=hPOcYu^^ivuEnn9?915j@|u%ZyX>O#Z7w^5Eq&d!MhMi4%+74v zgC3#g>#p2ukDWP|_GJA`YSooSo6E2&fC)$Y{jpVUTgh%=Nn&`p7^O4Y_z2LbX_{>0 z0g3_7FfTj{GQ+L#FNh5wlkk)ik5v{#?JHYOciYvKNDHUA`idqFfmWy-Bwupz#c9VG zaO{l>;glK_%mNBx?y45z;SY%ufDwRnyG|lj7@&T&qfswOpkSpWqR*tnmJ3ibojob6 z+7svh?6Tu*3NA1|z zx0Su23@O`628!)C-*ek_ciw!p_8?m6fd1f zIFp=zi%DXHZPlB(VuJo7ff(pralpCff{%h2O7ukVS3FoMT|l1Q1{Z4nIpehuPiHa1I!kMWGfTEpc+*Bvm!k48pz=Y|L5lq}slKiRZmtef;eJV@tBKy#1iqu1ic`)Nc7p0kj$Q7u~M~(((K7=3Zb})^9P!>&8<8H zz62=2t#ak$3qbLB=L!IkE0l{&5d|? zS15<4!!axH)rH|U-<}%}K7v|wFk@Q&+Y6=Di0&65*7FN+YXPoN%+r4++ z!SeV>A(%GFZ&Zs`N<5z=^|msP#g}%#VOzjbRACDMWOjDWCt2(%`!%y&X0I#yU8WC$ zY3ej3UXRr+1qI*&6Lr}~_R+BZ95T(F72GNDMYkp-|Cd|eG# z+tVzy!)TwQw77OF4%-gIwcf5uiGaX(Ldw7R<_8)usIKCQD9bqPOgj&qi%vV1XMaIDO~XD6 zs7ZL%o_IlA*I)<6WENMQNTSd~bSqMWDTTE~RHPto2*lAV>$bKeOQ;Qa?qoow7AY}A zMwnU%0`Z|u;#NQ}B#8(eX@Tu1<->tpztAZJ5s4b4_Nk@lBT`%?y8SqPS?G}&ZQNGs zH<1s|Y>-Nz8i5m@{2vb=u}s!rPcmnUuC$1fK|CNB*&5`mqCQObL-y#MC2{=W<_`~UJQ&)(+!<#kqHVB6`-dw$7Lzr5XM%gx?=#a*^uWBLXwt-Q=dn z8r-%eUQhy(z%yYp?(8w$mJmn{T{<6z@2H=dRnmBfR_< zm)vB-bxqthaB%}I`I#_j$Dz?f)3#q7bcC8kz*s2K$F%tg2kiT0uopNb4$i|#ui3~j@gr@#I_0{i(tHZ>5x7( zeb&Z1Yie9_@y{*2OCJ!0;`ySrX~;WsWl|KxAqe$D;& z-01t*zV{Vge)PL{`sCN!uRs6O_eMt?xncRV+^~EzSAH6|L^si8@^bYceM*;+E+vnV zbH$P4S;@_LGX><*4A+WtiA5?aG7uLTx~Htj#ut}DjA|4igSbR;4rJ<$Nn~|=#Q0Y! z0R%;m5t!?!!SGbMXSQ-UvG!#kEC5FeN(E73Ta6-_MV2Z)GK`=Rq{~G)5F`LmrMqIN zplcqlI6H?9dU;dTK!kvRZAm^D3Wuys3=!PqmVzE1^)^9E&-a#6MOh%8YDXqZC_T<{ z1$=~_sdNO{fz6rJh)U+zq7gDt90(&KS7HWHRw9APBOQ}WAsrtyZiP%|-2N|r`4_6t zzbtkc8MyQnwA<_H5SylYt`Wv31?>M4fT^+7GXM6k|6k`rbU-Utdc8-fBjd~i2g7ww z-C4I)1io<9@-ZYEncaE{1lV`)-G6I|MSu&ZVvHnlMy8$UyeA%i6!9oRs+1KG1{JbQ zJeiAouWG1V6bmN;NnsWkTQ(mYB!BTm6ru1RvYqwG|U_FI|^pY~@#W?i{OlTdw2LV|?4Ic>-LET~dEDRDN7$2A7R>Y)eiZ`l@szJm#`Uf>{UmIgh|QZ24*TklpQ{(6Th$VN@Mk&Ub|w%Eea; zAmIG?w2R_iOv6e+w+?u(zRCp5MV!8K2Ah@ep*u~5w05oO0;k}E0Dz1dp+R75?-*Pa zz>optLcUUf6jx=T7>(J^a?My-%~%$sECsF^y&`H8^TQwG#?4hhRRpmS2%r$iY-bQy z{@&vc!#HT9tJ-O8B8gXT;7D;Pnt|u2h0_IIrkY#IGL#6F8QGI{Wi+i~q%x_;h8^8O zx|GbqyqO43ta$#?5ulINz(IPjE{6ChtKlJ*F5>AJtb8}XQya!8vf-etTmk^g*p@4i z7-}qFixMB9N=s3ZR;T$jd1;M?I~_z%Gq{ zmni+?0hwIl(Y;b`84#ru>WI4{D7Y120c}z*(6D`jPRGes?2pM%ortGDJZ`N^bFx#c zd1$dfu?=E_C#0+;G_-13TA;S1St78ZENEAqTY!`SXXmsL_d&Uy7yGj@ZGFk}Yo-d^^R7HudjHZY-X7qq7ktsM-=TCZmr} z1mrJQsewt>3oL7mJSbCuhN{JCAWa7nn=H&1B0_u0urRRf8(*Pgkx#?Rj$s4HaB+vO z#rx`4V7c1?FWX=Ko%dgbm;F!5_pzPFe)*YO&p&)w%MLy8fW;PG*y5^>W5df1Y2#q-@TD*Rkiye0ZKxb{HT}sFX3d-) zz*Lb5ebbG;wAscRF1_RuJXHs=fEQb|Kq%STTqdCa!h@H%^`xE*0mDKVt9 zXwP9rW{cdqw+(Jqa?3v#xBE;0fxfDPqg9QqdXd51Ru8_g=%OpFFtX1g3(n`mU%vX~ zRb|`CKKunWUv&QIj%3?zb{O01%u?(=`!<(PVS~o5Hka)(L(S)$dc4hL0NJ*3!4`A5 z_LskT=F#x-S>HJPvI|b>dtZ0le7RHEP&2&jlV8t%Z*(NvFQuHywu?bWBevn1!qzEvQ%Vs+J|v zpayP|B`$?Xgh@iz1S>)kxyhNn(o;vupU;lAeED!v>@kS{Mo+~v2=z;_p~23_pk#S zVWe!m91nAn+xQVJ%g6me`lM<$YJmLVSFt3stLFU}wY6O_$3*+-V*F>ysK zppot>ZVD3=L{EO4kqL5p#IEA%Wf1L|Is}H7RUNn{22V+VZup7?aZZJhKVW_raa($T z2&IVqh+$WP6M_#{mV01k7epkIhlFHMmuvE?J?-4mQCvWYMphBRlUWBOGu1q%;uJTh z(!`M~5DbEt03w2js#&JqXRv~HrTqHu{>rwp8OqnO^a}^DS6uFkx`4yj7JSS*YSB}$ zQdV8eh#T!Z0^nT}H5$acXD2SA`xuclGsJX9LrB$W1_i27llSF#v%Ol)Mw`(j=u3BI z;g50N#JPfkbTlj=McELjh^%}$PMz&t9o)`1;q^Oxc9jXs>Uug9smNoXW+kJ6B?7=% zXPkTJL4MM)@B;JOt+Phh^+C!!_8}gX(c= zAK#UR-b5*8gtk1;+CR&iDH-3&0~Zl)r71Eh8%A7FdUM={CWC7a5Qo63va|=pT7oCY z)fbnJ8&e}5+E2}_mZp^3s#c4LB8*G`Z;QLi)#PMJ7clWGdaopT@ELW?51}O*x9Bku z#6uRCV5JM_J9?9@s$b|7CNH<842VO25!}ixt^pfc8vLawj*lTl&?WUv9c@JXzwfL5 zUvG&A9O-DTWTu@q1_@>>aeC1xK5j)&GOQImPr{8B*SFN57HU!1u8o#vXy?S!;iJ_F6vqHTH4r$=>6qAwwq#q8D3s_na_RN{xWc9_Y<14Aqy{qa_}Zh zCl2xPCt*n#)`2Js)Bvng*1)o*SFbIb8XR8o#q3^TmU!f02m1)5bf>kQZt`9;r0F$e zi^Cmq=KZQs6(HoYOMii%U_$qHRaIO1R@Mn>@<jY%3Y+v>*?(3L@~S zh}^tcTA+dbZS{iT5b?$vOtS-J!sR(*Xvdi=0%hAHwn0n)FLrS)xWsa;z&m>>*9tsr zYa6(e1YcScDxA4ECpD$xoofbq+<@91 z7*f9EywmI^kIr3Re2(2@8_j}<6dTZTZ@%`t^Upfwzhy)}tM?=&h$gN=FMI1UP zu3oot;~WBVD;E=zixY;ZAqeSgjwfY9AIP%L_^Oe)tx{L?@mHPntU3rOg1&%K$V!M4 zPen>0zzQFMVBBmjtE$nsRSKDcf{0s*6%c$sZn1xeFy}m5k=D7tq3&> z!_$c%)3LLH!{d`E1%2&D6Jk0Ar3^NSUShcwQIQC;YN?1TK0O_A@?egk6-y>!TdXh< zFz1Sl58MfX&*@q6E1JRGaw++)0zXPzmQsKc)F6eQX)fj)R9vojrPNMBatDWXTK*4C)5I%Jb zTSK+zij}TyJ*UM1Pf!*6e`Qazr?rlZRtu@$sZoT*B$CId#Xibfi`FSj+~zeM$M2{d zy*B6m)qnc)fBlz##HV464XrY%RKsve%3%x*L-W9jM%sqpS!g-fvcCl=kX%_()pW6f zjCk5!4YoN&_dom7U-8iBYazu(6_*U=)jX)oEC{HdH&`Q);*oby0X<)xiSiF!`F`H% zHuF+PA2d7r6*c$_^=ntO))<@{A&)+5O64tVHGR|Vw%H0kg-ji*wrI}>Ry&b2W>3W7 zvoG(qv)?N_j15*p!OQ;E66;S}OH020J9op*g13yfpEVOGGmd!KgO1Lt0pMh?BR~Z% zD|%gk{9IYOP~1suTid`jB#tNC%{%hlu<4s@s3Nzsx84#TBbW6A@-PSmabq8XQC#=2 zHEhiX1O`At8Cs@h2|I_8;SPsd-GaTfRk^a@*+(9I;Ep?Qwb@rM2*POmIQU@KjW=EE z)5Ic*qZa@iS@>xABHl>#)g}QeT}ki20lJZ*I()!zK&iTPB7H<$?TQb9O*}^@;r(XR zr_T%#RC9$@y~O7Ou(I5G7PLmGBY9pjVM+iYHd#6wl7BBB~bTRuDH?o|Hfs!~vEfQgBd^3VNTW z2soHNbPA)K?&H=~(4oW&4d6ztB1-95b!r9myT}lbNH5d%Bo3mE7e*p^J~5CDC3CqV zU_*^Fm1Rk1OvYzMK`s*^qllmXYX7<|3)Hrep%-Y)Aa0dyO;X$N+_g{ZGuoSm*LsUq zp`EmP*=TJ2M+5T~GXm{O8-toftls$cms=e0>4bygD$2;Jqb4mzm40{-T~-QurVuV{ zD>r>bD1tzwT$F-%D&Q6?oKaQ~f%JMYK2R-siTDh4jlUh}MaE0Wm^IFbW#K9Gq|`-J zoJFv$U@}yT^cX54S0b6yUAmC8DI#8OtnfiOo@RX6G>TRBA(9~F6NAP%K4_VRCTq=G z`>bt?2r#leW$zfl%R-;RZkKtuj{n}*FMj`hkhxEO0nUB$tBB2IuPp=1p=Nm5|Cenp zdw=;yZ$IF&#V|Fr>j<~4YThZJ z9m)ocVO2~JL#E@mYo$QAfVPO)AC3l}&9ly0n?@Z1C&sXYY{?n{p1c$*@Cm~=whfJp zcR_J{IL}T^b7LF}Uwi6QJ6tAff=P794dtYX6Y;U}#FKZcE?^k}N18DFZMm3}Geqz3 zwiDX6o?9HSnJ(@R%j>MUs#Do3uej9Q!}f0<*_W{|IPcUGk3SrOzTsLwvAXHBQ;u;+ z`|LAL`0C!fc$Ay1zvjJn+z2Uqk=dKeu(J(iw>||X_^XuPOZC++-}^e}w7Gljy5(VC z+vT(q5B13}c=?v=F7p572flsNWB1?j_=9&jjO}Mq&STGe_1WI>)I0c5oG}rdqHe-{ zGCLT_jq}oVE6yILOFGZ3g{iz^U93{bB5)Pw*q9U$=ZuNydMKp}Za>~1OTn<&WZ*1< z9$P`#$hxWsf9Wyo*7Ot^8Yw0tP9X0zHPg$jKtZ0loePdSO%pYS3Sw(IQx=U<2m}#z zp^>hXwv7ogl}cByQYuoZ#DXkyrGg?Ug-l>`OaKHyL=lxrH+7;U9>MZjDFUTBT@33n zP9{Q@Wd{3HFFrV{5fhaXl7ysUe|!cVKORwvf{7|Zcr=(Og|nb)=#r2YptNTdA!D43 zDdJY5h_VzJ?2jh}af^RuQ5_oIFQs_t4^md=8Z@QtyF4)=X7D))1!F5#yso;%H ziANoA#D4qiVMKyE0B2Q$)hx_mXd$s@9S7;EAb{5m1a%P!6IN0Y-%{Yg3bt%Q zc3dX2KQG0^YWn_-XE=5YJ2y3FfP;#jM~c84jZ$0+?~Ai4SA;22kou}7Oy&}oA|R-Z z(UeUHPlehE8j0-R165YsaB4;%d4ms+JzUI%69zY)0}o9A4jeqr80QT%M(m0PWtbwL zpGPOeN*0$7eSx&(XI{oe5eSU_WLz#$I-JY`lgsUh}Q5Blm?v~BaIC2q&O zamMx^fBiQERtly0i|HUue7qLT_!gJIVZqr0DySD{^sdymIsu=W4OIcQTVO;(POLly z2p8O-2&};$vkwSecJ^$!rI+;IVfwOzz;j~?K|9g3GL&|rxcw|>#SSAJ#CrTdrI?PZ zZ&cZRmpNVpTV%lnt=@Z^+ZvZM>X1IMshlo8C~D%SEdD;UWlI+v2AQqf`>vNN2mqzR zzjliKF^c`kj9%Da-L)N%`^UfkTQK2EYfn{B;wdK_M-p-p!z;|Eojm%q__}Ma&EfHu znJ37-p((a?kZotq*leTqy~6C%UbKde)ed5BJ!A8=)>wU;EvGBO0)PFV{=(bPlqCu6 zvJi>K#lfy0K&>tebq82RZaaf84gu;Ncn$}` zB@U1oH-_~zEi&+dEKR^)9<&jL${a;bBOGm50CpHwu15n_JIC&AaRn7O@@N$yh{3}* z;4qYQWtpymx|QOtK^0}CXIqJ^DrlA-5I!FF!}s4i?3_x}%&^rlJx6ODf|gTp1&OCr zKqIu`T129<9vNZuRy{~}Fb_69)P3~pI+1ZZ*T?`;pDLmg>AAY3HY^=YvmQPe>YKWr z6fKab+DYg}#H~b;PYhuwdWn#q78)J}5gVelKq}PxJQsFzYe7j|tG@aAYZ!rfbwgPy zk~?V1_#y#j`xL<5zUXDhm8J0*JdL8o?I3HD+LeR-Hlw`}?;lduVccq3 z6|_uL^JZ0=284S?WDsc7?Vt!6OLMZ&Q%$7SA`)H}LHrTCtc&$nr4InM)`h<;MdIU% z1I{AjB@t&Fm14y>B8q67Y*;TwP>_oQn-euMDMP7tw52GXs7eiLDuRH73JM~v4|oJs zs)&eC6){0cU_m6Fu5MFSB6>|CBM-y_0WwW-2pZ}TB#LXCwdb0&xL-tl@E&Kzr#OFX zuh~`jlV3Qb^vVB|`qdx(7-R;M9n1!t|LAw$75?zwyz}$ldk=`Vtqd}^=?pKw_3D$a zJ@?QD^PacA{M-}Yf|q~t?xVL~f6|6)E#iA$v$t4byXh_MPW!zRHeUOmrCAk?b$ep5hbPK90TT>`D9VFx8&&j$3?jry0xc zwf*Y;Ep_nTo7i98Xq{!(nz+QL;bjO8hBc+yqJ?C6ynv+5P`7qhK~#<+e^T%&A7m}S zk~~JIwR2**no>YT7#274K9|x*Ma;jzqYq9lvFJk9Y|To>tB!L!yzTRq8?86Z7sK}2 zeHRm@m!J)>rYfkMFhFr{H>>yL6OO4~{CP?cz3O<*U@1980@TMJa|C~w%%*j)nq%xM zpq96GJ|V(gAv_~AdqT(NXm#1Wasj&F zzu@|7Mn|$iW;(*rEbN$^xgMM+v)yVw3}HyF7Qo!&paOcnu6MLO$3;Ik-}DXF_9F9r zciwctSttA0S04ehv+OXo6WYE27FZU!`qHzmy5!9L_nLFY$#c&?>+9mzUwNJ{WB&#( zKXCiw58e6fX&Te+sjb0fB)o4=!{m<2>6nh4Aex(^poigYQ0xRIX| zC5Ye<6Y1m0a8P7$FVVkxTy;0x$dh+>a9(m!}M~S-c={R1B*I+a)oarl3WaQS4{h9}Jj|M}+U`009s*u=&>meebCXE#2qKhy-O1C1i zxTzTvan4AnXv-B}bP5|uR-a?-k0GE#vLtb}QqL{*l> zj3?^S--!yI(}lpqGjtf8ydi==AypwrCk!(pla8`B$)4$}?_e8Qk1`9pBKFW_sR*%K z9JCFhG9Bhp$Rih-t2pB`nsy}%TUR_KB47}=b&z;wr3i@Eb4*T&=+&j&iqsugKv1tL z2%r|kT7_Hbnx!l)tq2aZ*0iuiqc7m5#_^9BXBv?${MskxY5 z%py^idR38Gq^MolVO(My^-{koREH1FB3Ln$TUscUcb)I8p>R?7fm}q+_TVH-gq22M z-^sl(%uZn1VV-yd69%PJgbZ^QBPyAZF^hOU2EmvDSNpkx*cyoNWEnzY zi>D$Y5_=+;sIeBs$l~}v!kPqreelkluzSop7h^PgdJDXK8+c*IPkgJ_DD z7o?k``2)5X&`V)PGJ8hwyIjXkSj?J(EZgEHcSl?n|H8tHExwQ=&EUhI{OafM<}d&F z4*^mEB8Ggy4KNeFuq%fK2I&a2ET~zoTXEn*7Gw&iNXIas@xu?^p7#TL#9nir_sXl! zKm5pdzxVhWURFn2Jt?_DU+KDPW zIFH!Fl}_@99=`8ex8Fqa*IsvO_obI!e)feIeT)11zyAln{qtY^{!f1DTLk_Y{XPX> ze&sm`%K`#<7y;fVi3KsBUfi+(bIB5M+wLbJ6+ys^4^r)W57`W5aMDT?kq*J#aN{+$ z=H<3?ZIBIE!51RM15@ECDWDDT!dChPV`|cRO>Mg|N7tsK=Ar#)?b@@RA|TU5wJyX0 z`?%GfOH@__H(4l+LWZVR*hr;PAuLax0!$R1f*++MFgYc{MCd`eRB$VxF~pHcXsk2b zVQv_-3KV2+eGdWRkXI@bj0%L*csxY?$V1e4oQk0%O{rYxqP_?|cpfwNFuQDCqzHoyNu^XWR~&J-D0?pG z6HnHVODgpuK!(1ou4MbfkmQ4=s>4KarXH_?vWzn_>{!dxI5kU8QxNgcOE+d}mf~8b zpc%Ax>@8;B`0}l6d&*8?yM>o+JqrlJ)Ia@^W2^6j!vHd*?5y=WZ#)Y%gU|MtgoEp{V`^*sLpYJpCL!IDTr-jWs z058{33TP%SH#E&NH)#s8Oo9+x#~QbsKAkTLGYWtwXvw8ii*t;SATno=oY1x^YZu1? zqg6z~T9#uu&P1W~4M`ox(v~G$>#;U`K}EAaS5~fr8XtcxF1QREQ0bD`Hs+V1QNx+jX^o1-q+e<9A7*J1meCEuaz1%X(s1yLf z93`JwDOy_-kS?eQvDr{r+u!D2qh%g~$&veU>&=&3dHI>wTy@@c*PMUJMWY$IT;dPQ=b!rxc==nmjOxN49~H5%2(Zs7!rwHFp*e+-&A;3p^3z=rvyZ*7@bdCY zEetG!$$bYKQnsP|tsAbevm9#nIzLevR|SJtMrhAY$n2*0o-v6G?1rlp<0Q1S0qtKZS~y zD1E#yRAQY`yntprTO8c#zd|(*1mnpFP>}nHfg{Id5{&Z*3<3l*xxezF6NWqor$B*6 zU)BN-N5Kg)cv<+tn=i`JO!X^G!CXAPG}J zM8KiPhck1z#VT{rS1tIv%Ej5*6cw;NdhQX3y_u+w=o(IO&`w7t2_x`t4NbgaJ(Lf8 z?>+Zu0GtyR#+g6QH`{jWt-VD8P#X$>Wg{C8kWDlsJJh^oD`pnEEbgh-NpGT>1?)$w z&iDPC#P*cU4#}nnEpR~0EcmFA-3Un?L1eI@h+9U;5oI)%A-Uq>vItoUVX(zh5fvf8 z%GE7;WD*kqCB<1xqFe&tqlNSk36G)l9;LX7;1n__jz9BaD2Sku>p@U3k%ot&pj_z+ zCbr^gaf>pt7!t#+L`Bk_Y?wTtP(~ zdwohAD`nAm3LXJU^@?N}wP!axm7o<^ z;!|T5DuHqT_`pFRhAw7kQoDD)JSne-lYvyey?`5cYm-ekfRX>*pZ+m)2KGW~P##!k ziwzWo*KF*9b#2r_d0}K%>5AARwzumx7$^=ngWIkMf|GFZ_uqIK@Vw>L>mPXVJHTcD z6Ve2n!OVN_yX&5B-~Q-h5AgBq7w~ylkNv6O0nwe;A%$nfwwk*6%VVMlSSqMay#OZ3 z$xl>x*)_4j+%7LT5Z@W)&g(plB5#D>r}-+J}g=bY(%2u6nM-+9;R z?$_b{=bnF3oEGGFg`HiY>2}*eVAfz6^$IXM@ub-3bY(Q6!t(4xPQu&PJo}u}&+s3X zZ{GnFqk*yoD*$L9=)3P>oCn2$Q^Nv45lDeezzQK$0&WZ_CV)d7%|rKONeeP2AZ7w64pq-> z*kKcIYFu)`d7EtTCI6pw>;zKA-!Xvng3xJ~!7*23_%Yk29r;U4{@=j55BJ$oMj&?p zjN<7oMZ7GEON@R8!* zeH`94ec06nX5l_4yJOEzkvV{E%slR>Bc`mrsyBL|^@G2=oSfv|Am&sf z7R=)gRAUZ`(wbXtkC>-zLNtRoj&3sx3MLZCEtjzdNCNo7%N9$e^Fw&uYGeW2Y_D6s zHnTv@a_v}|Yi_ye;;XMb8(4;v&pYRY%Pu+N_S>!!yor3qsmI-L?PcHn&gfUpm=M^S z0C$~8jBE$oh8lV3MN?BVo|er``s{CllcL6{6PI^Be&PA&gN}XCtCyEuZ79RY5?zDO z(DX&;d;?g%_VRNM+;?Yq`O@>ham7WaUvTybAhWMzKYZVN9`@Y7E`V1N0&x9#)VPW7_n)4^xAFFntn3Vb-C z)ah|lmIbV$*cO58EUIWTP9{4|VReON^cSs#b3#bMn7i|kMO1;gL=|Ns&%&)l{0IAs zqm+&V)x=#U_0?B!vmZzXsYXTCzH$f5dL&Sz1-EhX{8##$>fC2}M_Badt7Jyp! zcZk{>ANrJ)E}O;1f5plF5C8SA^=k_bI<=ReJ)_=?CZv%;MYiXRZ;T0I(1>xU?@Rtv zHdYXo(WJAj>L}Zjl}@ir=h^WRoebwdqM>7b!BxR%n5dA!@pLN}O%DivDV3EnwDv^O zNVG8~6;#2Vh-TS>_7&s(E_HYpdyI`5aDy?4Yq;8ljNjH1eQC)DX!;3i99+k1T|I7$T|ENl0l25 z9fm4$C86qwN;Z{x;sT8bla=IVdGtiZFr`Sv>#)eIy=mcIrI)4Y*5q_04OP7+Q&*gI z7S$D)4ihzcYII}@^f)-GM~(eY2IT6w+d-xPsWS42{T%Eg1;L zn1n}07?i3I)aB7AB})V!8o}vxTcFjBwuy`nKq)was31II>SLt{=86bzG1M4X2_H3* zO>ir>B1#F2z-UZl+gFD*;8<9Y?m>_oPm9bZO5>qh_Eenkt=k(Ik>SQTrd>=7R!qd6 zD4AM&BcySI4ED0EqB9*bK->6082k-QRvUb8KhMN;Z!9ug*hEqMu)Ksl_h1@LUI;|Lo8I)GjTg4B|n2oni%hCBmNJW$|zy0*DV3Z6{YcNC?mg zBKA)O5h(J=SK<9ZGMY%&Bx0b^o8bkZGgj5$v2AC-hle*Fc>Le#=(P#;^6Kt7+*%@D7 zFbLEyRIN2)-a{YTGWKSxuSGfUJ&)vc5vRrTFFYmvt$D9O{dBY-Kz@r1?9;wBcphG6 zI#o*-fMtnJjN7jlu_4ZqVDjNd9DM9?M}o!>H)2@`1crs6`}Pz?;0Q=0{1UPND?%}$ z6&R-wa?x_A#%<=pSq)$@EmI2=an*Qg!|b4`AlBU|F1;XbxYGzLrQK@7h?UhPwM-ob z{5Al=m_cSwC9p$IA`#V997dBKCXz_J5TdJi_%7V!8d6K6mIqrxR1pkogmjA`#Vd

T77IDQF2kZeVBGe{;Tm%#r?~<2<}^l~NdzR$R#D@h5+> zAxMiP7Z({c5+{5`MBEyyp~aMy;#NFvaj5KI3Iqk|mBLNI3>bC>B;%lVxr|&8ue#1i z4+p1{(5;A}(+p%zQPzp~L0X(Kk%%)UC<_FWSU^04QsfrssG!_|4Uy| z?FttI%dUX4z!>6!2!L!u*;casWuVx>Y+xBQwyGxtqQlG1V@nS#L(L%bkKTOr$8SA$ z%|*w2Y4V~wZ!=Zcapoj=dDi9=gdj733=7*_7DRyMU1zMe)8?y)1Ir@2Z#5}2eZY=i zI(WB@4%>CTxqH}do_gE?TOa$iS@80VFHKl+sZYhr9dd?+ZK2ldb$npi$O)o~gNpDY z;0zkO^+_&Xs0dFLEH#6GwD2S68CHeqnr>|M!kZ(gd*x*Zd}S}#(j4ULk=I>yIWX+o z4&L$cev`z54%oMMqHQ$$UjrF{UoT2Srf{Os8z?3NfWSSr6wmb)fq6}u>Xn`hXkd9V zsMo~Gg*Lg9(=3UDqnqj?mD!E5IQy=b6p-8uLSJlUnM_w-byYS|5rRb*TEN=1jVhoO zvGZV-myxNZFJ@b3Hc0a4z5C2u(r>)}f@`ln_r~ily5h34PCxaiZ=QSN_19h~_&?O) zhwOXJl^5NA?;UJQY`%@DpZm?81Y_hqciYvROJG{V>1q^gl)EJlgLBsp-fxeEJ~Q9a zi!E~62}icYY#-S!v#Yn5`$V>PnZf5i`2`x=R^DsZZB9Swh>OlW$$4zIu71G`CfiVk zl!aHGd-#o)A9pI-T$b0wMagwB`ibZ%A`*?^JORUd1Ae(uM2wus1X*qirpxgGjog7o zLCg^}t;mfa8f4>n#z$5oc1+v~6?Bcy$FDlOptGY^EO` z#Lk|SgO_iB(homym*d%PC8Dv#%-<8Z6MT$(W)p|0ZRCoNx%aXLCzhK{RF#q(L0Qbp zjO;Aru_(x@>_BXZ+AZd;_>dDZ4wRLPp)A9cnU{Gn*Uov`ZSq=cvZ+BT1m)Qi@dSGQY#-7O9Bsp|B@m%nQ;duDtTtj@1(4H7yWH z2TWf>oWX$@)WIr4;VeRTtVp6324^wP5e=;=i$GgtYPGwKGLaC(D%sGGM-?<8KrLKc zqAa(HOGnJSuIj~H3Z4SnDvLAQ3d}_eG}Y_Y6CoiQWo2ijD+}* z0nHFplxoT{j)DlJ;K_SeJ0bDGO%~@+U?x2+&of7{bXVS~%`hMG_0k_QT?scrG9zx9 zLA{COxKUQ^DWy6n%+;)CjzCXlf$$8480DZfO-bl!a!V|n`u{Sd84TpB&;l`az%G-Q z?@Z#XYAVFA2G&Di5S3ai(pEK5r%kZZS+rHlzh*S2GOQk31#-&5Jfbg93Lo7B2Mt~q zqYm>DNjl~#VqRiUoW4C8^eSQv!pk+(DCkzpv1X+@OHx+3y)>a9o1me!r4qFu!;>9U z#AsAg7lv3dg1#wB)q)~|h!lZ%95&n*{`}{cSZpC%%qnsq1aktj6?F^(w6==%umAr) zTgO8$5t-MG`3BYp)MzyDP?lbDNv~;GSEP}8jh{F(pa=;a{KlE-mRV{kXG=H+B$t)h z>FYtj(d>!2$H8MHKhMDl7GpxMcRAbkFaP{&586Hg`>!!&HCxP|UvU28jz0>@b0pgp z<_!gl2l@cFwr<@9QPF^YZqY~?&@`+kKzWsgB_U#P6105qp?iViXP$lh-~8^6-+j-I zrTj_C%lkjFEo*=I#+$BjB%7KPXAoCy%=0xt=>8gy^#MMNYtO|~@aQ4Xw!&?2tC5`E zV1-5jWC6eoby9~&V6|qixS-!*j)}TFN6kPRHVz*|9EZp# zCD)7FwxNORb_PQ$1Au}xiUSnT4FFOQw;rWQ+h1-;P>-lFYZ!V#Q>Vr(QEM@*X-)+x zx}oMRT`3X7vP4`J(Lpr4Mklb#8uV}s#+sgmJ{~}5h%iu;o$x?qg zH?9hX2Av;&s)^|NtW6AoNJAtPIiwIEjUtmPMZunn+wlcaQgEP@8y|`B8R(-aSC$ZG znu^1c@sWsU1g_a6CgUrTMI^h)8ccZE41!oKuClPDxG+3BOg<7~POns}Qi+ifRYYP0 zA_UvBvXHsL&cFPFAKGGeD%){uh`4Wjz4^)$15y?NoMi#j?Jxi6 z&F}qW-V^Y$&1GB6SDt_5l$Aa^XUj=D&Rk<>zf_(%2~ys6`bu`0x8HJADA>{K19#cL z-m!gTd(H6jj+?D)b6I5P>8rxaF!gX?`{13X9kKfc`^;YB$bGgrYX6zeV{fz3#8sDF z;M4Y(ZDj&z03*B%NZO5Ua~YzPXa*E9(Qr*UpP;C9V+2;b9cpr*5%g0^Kn(YFNE_$_ zX|Xc=I(O}3US3}I`3tj*EoR@%)`PsQ?2xuiXP*djLfaaYliO}hH+)g^uiakWk*ZZ^ zQwb~RSeI0bNgpyLwiFQ~JXFi;2)W~+A`)_M2#-v+^=%*XpcE4*r6hU}5gAl&x|f2d zDZ_F!|6g6;zg2pNIi;I7af5BmfM|>fKnizSaeNTi*CsPmtJGCjoMnIc<{L%MIrH>m zF8Joj!17gBoO{ZNNA5IdtBWr<>z+GrCNrN3UWV%l#v{U+C4qVZ$ThRHQ((Q?5`>|+ z4Q?w5XPt5^yljj4hy(Yro9zF~URd@)?7MHh)?sWfH4l&(g0|6o`Gu!l^37A=<~-c_ufJIEO4C!1-XlEy*thL0zy9K5AhRhfSI|tCgJAvW%7z z1p|9zA|)C{^N1pFl`;ez6vxw*Q(;EPpBxA(r8+_1lwMpqwsAIG%No)vtM>mxP$LPe zy{fr|CUgs8j$XsUj*;l^ox28>MV@)`L3r7LZM)A11pCWMnF?9Syw73Fl}Zs=TrYu_ z#U+T6ki=7?Nkl)8RuQ%+wlFE{)T*Zx1vYgK(fLygODFc#3-`6+Q zXbFx~x+G*xSqD5g%zSA(XjUg!jP){?;slez=AIE5L{!JZwNGRc3(|YCip%w|L~xK! zHbqzl0RzhNGzGF%SGoAquFz-3QdBA^OBfMNlsZ+%;EJe4>y?g13n7(U&&aM*yP^q( zOzkySRfr3+Fd;@y&@F>gR$b_ck){X;iF3M3M`lb@3re@x$`!|1oIFAeTP@_l4Ri9# zl^#7BzP*QqKeI51>Rb`iy}Ae{B4CbB)=)craUO|1cHEj9!&~F3@fAhDWa(T8{*2qv z&h%psd$*3I@W)D}(zH6ANRt*+L6KA-fWXcSND`LR(sUUDAV@I6D~<_aP~sz^lqU;t z*&a3WBLI-Kd47niCaUpkf#x*@@j*a^9vRitC1fQ$xbYTnciQ1@+k+rp9C|3Xn0wc2(bCY zlaJhS=PlRYa8(_Fc{L<Mg;L)KOUhyfad!(cmekwIWXXR}$14B`-Uwh|{86+mo2+di_gXb21%`*pRiH#2)W zg3>+!4oU<)TmcFg1daeGii0g7kpXAvcMMfKXEd79S92X2SRGayMy5q+W*VMSdbieF z(7H4vSGmmvG1MPB^kRTAQVAmC*ZN46NCOTM86QYH03vbfpa=0Jx}ubbtwlm(oz3OG zbq_Kd2{Q$HLBVjAiBf0L<_F2CqahKPb8bNJa6MMHbkP!=CgtsWCmIG7!L9jku`{l?Lgz9NS6|sh!oN$&v;7J336TA zM26xAMbHaF75F1#G%jW6Sj4jy+-j>`JyW5k>pIr>e}jmf%C>#Xg$9#-_{({05e{=_ zv%|}<^S}N1Pi!&!0ybRiYJb@a%doS!AP!2y%cGs;vG&d%rJ9Xz-lfA+WH4DHpq%4R4#ZF@Hvc<$5HeJcp{xZ}I zK?jzhW+?}3H`NyNUb7}0{^iZ)?z^SWh0U2hW%2|+`2FPfmrd4?njIE^*VQI6oD2V& zeE?N>69zN^nH|JU1b{Pm2uSji1I6GftQwwm^l|Y;7O@9y?3IFEfAMO+pBPS^G|^1w zGm_r-*MoeK(ti>^KmYv4A3fK{zU(`1y5V{YeC9J8U@sh*1i?>ZxY`MWj}*)A0*Gq= z7it!VG(}))fEvE$S`cQJ9cQb^XUcdlhZU-!E1igSB^x&393`OaIe zkUsjK)FXET%Ywt$!1BD;o-)*%Obyybcq6|N+Z2#@+Q{Ej%Zu}xuvUJK=6q{6**w0X@ShN zGlfCH_LK!HV-8(|$+|r33^Lny2ARFWY=2phYXl%OAHzhzm*7msgb}h8>*Z~#OQK2% zqn2t+UzFKUZUqU$4h1S;&b#2ouu@sBI2fJGQ*6r>v4p~j#n2?Bop2_-x2WB6QxQ~! zdht}2eOy^S@3czM;F*MMs5%6MM~Q>7*^`~q*F(`1HBsIsKhVeF)u?KFE-H2S84fjv z(idA*2o!-G1nv_Vuq6+fRgHNMQKizeB%i@b6rNm7ZxLF>!kI``t2SDHeM?mo#}hZ* z2SZ{z;^IWQ&S-r}?d;EJlwvDRGTAt>zO1VYGo7-uAQ(~k&jt{6ag5cDo+JvAjbp4o z%Cc+EKeviS(~Z_&7gFX7OXrPq-5Eg_?)fUnVglxLBr~PNanllN3{)m?w;3OeM2e6K zA554|mf$Hc6m?=dTn8GRNFojxN<>d_OtP?B>5BJENmA|VLf~2RU2%|(i7bg+S%5;L zQdXiY#nE{B3F8{QN(-{^XVsJ{moVffyj;Pp2d%6Y>+uo5Jd0kXQKC@`>{i22DPh<_ zKqgU;BHp8SOHGz^#mzNJ-0&oW+KK0u7IIzjq_SG1+tJ{vN#d$;Gq^4RXAVq03@<{8 z;}ino_!H7I6|iz8EmrEHm!Muc5<|;WFB@u~x-ep7gp?bCAVy}<qx88nqwY+3;oAO%=4jpGJh_7BZLZX}*UloH@aeRH`7Pvc?wc zZyKopn`i!qe30?afJ8K#0lt|1+$8FrR#5DWDrEdtUG;{7ii&a5L_~ur`;i6RHks25hlS_v%F{hbH|H;Sn#BJSmLxGE!7B#L)r~Sh}f;H$k3-zN?{&e z4l|-pcYGp1k9h&{|3MgblUt3DGT@A-xkkJyFhN7p0?sjvr-B9Pg>naJo21;Pom_lO zSZ+TtP((TiFAzXM{H3@mD_3#ysEeEk8tWROL1tc4T~*wGP>VG+h`WxpST4kykTSds zBlqeenB3`W=_1nm4j8;_bJ=0+K(QB=`?{B{t+fF`g&rRXweYRhJ#~Eu*T5;h|!^;pQ#1~A4l!r(=%aF1ZFby6w zBXiE(LYn3sb1n}S#MVuKYp4^@^YzGH`~hiWMWM&l1Mr%uYeD1 zI+IIm1t|!4a;mMs<4H}c1bu%S&?dZs#(7glpT~qdECuic9J=;Kw#_DXD8pLv`YneVyl`Wvph$giM|ICQ_;ZuZaU(FZEk z<#4q#i6n~qd<~;8*y39@@3)K_G+`MH!_R^I=i(pni8HTpuqqOv=|JP9T?Coi6+&CT$5(C6CfGT3uHj72zjtY z=E{M|wUDkx1yhs0ig19<&X|~FwMOlPW9CG(9xnbYKn7Q}r@K-^kSO3l24$6sLzEI_)dmz{XAV`wJyTb~V?vJ@jYmL? zo~cB)%pxFTL(hx89*`6=r@lsoK8Bd6qT(b`98WdI*{wt^wqBq}%o9HGG#fP1>7^9I zRvj(K!w$)h3?FroAtvNWi)vv*>3AxR605<6S*%jHiD07AIJjn2DHP$zsfABy55<+Y z$y?xlOm60!aBJmTeC?a6fHZq6b8MJYymvQSF|)r6DcZ>q0oow`~h{PnNws@qM&bTnTyGT zSm4fC&eHwOq$29lm)QzuX4Kzf&dAU@gBV`u4ir1KZ9wxBc!S!Ny2R}-n}YH0Qwg;h z>JU%kVHsxR4?x^TT4z`2%QzmcTErWv2x;u{UJ2RcPMNxTNO{uaRi~}H=F~MO*{%im zMq9CCpctkD_kzrzA2bK|d9^vb+-YaP5R?Ou1Is?Z1>CtEgRXDGl7g*R+sd|UZ6(9S zz_I{M+pdK`?bUiy*cPqF#5iPbte})GY&f+RN=Hr9lch2OHfK+GQ>8viMkH~Pk2Yb) ze$X~RW)W~6nnq*5w@*34=T%inTLI5CO-KQ;6ag}FOOc9D5HgBD zqFm{=n0>Ut*a+SNxpA)SP^o%JTZ2z20&CxxVm&tIu*g-ocrk5@_sdI^flr68~@4Dv`Mtz49a>WcZG$iP3Xt9`r-NiG)?76$Em zri0dTz+Bb9vMpvs(7Vzx72H-YV(H3$B3?Eu8_l$Bt+c7Zm@IKD4jPPJQnrKaPf~W2 z>^HTw1S30@J-o=we-?&!lbyKk4Q0?+r0-*Yu=Tq=-|_;GA!Xl?^tQ6k<=u4M)h8Zzw3!7!hL`;d`Krq< z_RmBqV6P$9=&daNKqz6jser2SN03Oc(aGn_FXCyqLh*7>Iq_Ji*;vhuH(_xzXhbBe zj9t7DM<}gD1NAtY&P;i#!(T3L^p)$aVXu-=)>5IDkMMzN=>k?8`qTn$+lAr4n4_&B z1G!Q#v9sJe$T#13k@uJVM#^FAlTSR%Tg*Q8b;V_8`HpnZ3Tg?;ABVeIqIJjpMm zoWAyfFG$%gvrm56Vs=zp{G8K{@r|#!2k!+b+h2AhTe$h!3vayo`~fdN_r!hnm%T*L z|D^Z{{5`%3H^A^H80rNBph41YqrG8Yid)m4C(b)63#GVRx57}+(AwyVt%9Yy>U4Tv z#7YTL1hoj5qtQ3fxUH1pc~;2Kml%IppBPBa-xH7#&#X;H<2iH;3+v`q$zWvg89EmC z+H(8OL1rU>MKeQzStgE2jv`$)8qgNJzAi{8M z1j8!Io?{}pO0Nht$>xi8-XgS{!==>tRmb!6-9HhXPS3&+2eoHg!stf^3K~hLFNW&% z)Kp3rYI-g@6neybT3+bqKRf!FGL0Ol&~g*|z zwOp{_dZVAP=L%xTP+3gvj0vZxi@t&sK?_$k$qPOGT7@jN*99;^tnpwdQKjV)z6ncu z=9PlETB64ge~L4&f@P7MY;1)Lf$HK z3g(_8&d9_SaU}rBRV{*>tFj)t>WCpr5$PyZS1o7;J|rZ(fB;)G#Vea4)GVTuidaS| zvXLZVq##Ha?i^_yw6@35Yjk`Go(5l$&%o=j-(lB-J6ED~-cIu1P@sY;D9*@yet{zD zLPi)7Dv$&*X(=TU0r^SA$aL4hlog?uT+h_o_ZpIwA5)bg7Qq-C2Y?PYC!D=B#K=r1 zAizx(%F>QJD#g(8rzw*rX_j7+0u?2y2p=@2Hgan4wwMu^!EJyN1`Q@?Fd;vY^h6!D zr>_=eOJ_H-Nzob+la1KEf2D6OvDhM(cBME9LULL==|LUqSNNwUEwoHDxTU_hTF8JK z8K}cb+Otr@P*)8Dfw+POy3c*)Ge$XQ*lfE~88JT2W?S{LxMs#wBYI-U^4wLb0qePV zGA2l?tZiSE`g>`Du$ z6S1{Z!!l>d6v;AdDBz9bYTDK zXTE2lfE{37LGAX4|L~7~k5r5X^U4h`+Y@Iic$rkzA{fg5e_6iu2Q*Ag1;F1vH5Kp_ zFs%E-5y2u?AY!!dHinczkBL$@kiyQj zc~(LuSG)3b7)J`IWoQcfgp6E6J!A{-7{@|AZjD`t2`q>pHi)?r8yRkfwFqbwACw}7 zK5w8H9*vEFGd_ZXIHL!q(w%HHA~4Bm;g~Sl0+3M8lap+RlAiaK7NSEUlAz?$UKBhR4rg>7+6xdn8K@k;I7fH|{?g8QEHXS3zP^1)DRW}GcWJQp8 z6{%`$nO!`k!pMkmu?c+f~-n(#%bk_r0&3wwbiYoVB1}Tg>*3VPOE-7IS;b z;bn>7v)n^=-&jET)$P|jcAw3@w)3>Pdrv>&D>IzOp0)96%P#(@-}?gAz(A-I>^07V ze)es_Oi0jfDGwPE)bUM%z@x1nJDz}>m1Ka%d{U4pz*0Z4-#O5KG<7f)k!^_z6 zPwa;Cy}@LG;ugnxeFxxD`BE`1neNF+VM{4+q{`Ph%F8fSeE@|pZvQ1+6ykd z_;jfGmYXiU`_Ahe$v*4!ufxmNUgd9G*V-;8yd&bgmkxw8wTWieowg$Fw6hWl@Wia`W19m&< z*n_;w?Bm!@V&8t#Ww+gMiQh=|&az$RK9LPF8|!#wd1B@@gM1yt0BGVfDjLJ3=uiep zxefA-nN12(nvM;a+zde^N*r{$W&}(Oyp5}JaS+dUlJ3@+?6!0S#dT7-ads`DU;&LP zTn7%)#npv_35fg!Q#my?RI>z_3<$UU1sB_5?l)2$)wcVLKso{=gUt&eJj-|GU|=OA zt=hF->6oiXrLq{4O)^3@WZ3)%&I3;vCI5?;r9h7A z5};#?W)6{B#?hzjYH6qdCfP8AymTcz)nbUy)YXLvhMuPCq~pd2JT!?!{hxf@2l(ez zb?0$oV!;+A*pMy*C<99Gb!M1xh8ac}K&p%&U@RaO5JXUrB1lt;O7Fc%k&aXW#hPT@ zB%5q@v)LrO$!?Nov+a3yU%&qwZvGi&{5|*iKKFa?x#ym9Zabgvz2EPTUqMtBuu+A- zhg}X@tHq?mG6c(vgrr3PjVa7dhz(LB#Q^l604ofMfg1wT2m*V-oNBjhNO!uhEz=o# za$$dLEedi8X7ieBO$!s6T#vye92G@#+Fjv4W*_0)}bj-Ci zvn!=)K?+J8QY1;JHnfYaYkY{I2vM01J!SDBsvtd65G-93L14&$V=9%a3D8qkPy`dX zS{Z#7AU2I;xg~=Z7r~k2SkWC5@e&CmNEB$7f)OBAiaJ7unml#7{TBanVvCmtVsn|t z#Yf6Hl;V~b$FY<(Nu0uQu5lm|$+2b08imXf5s*PdbyGE5ZxqnNEqV&l&Mg_FBozsb zP+Hip)R)Zsy1IUtWe~eHkl|&k57S8zbV3CM@-R`Tgfj!^;b7qd2LI--{sL#6oP9j} zVLilBAR|YSt$LP7i>A7g2arT;%xZn&p(ilyXhaw*f>qBNRO_hPBy9K)4|oI;mBq@; zqn4%!bCE~uxEYdUkj_4YFYcBF+!9{nx>cNbYD_g(1SV(w(=arg0c7rk@#xsI^CaqO zCXov%cY>Njl6R-9frBCKsDRQfnZ1JH1qwhOy?U`FUTVs%7N$0pX$~j4CDztPy=GDu zL-91&*+W8J0RqJ-#bWv%AIWs#8NxsR)88|rdTH&%?+!a|<5V`hyve2;t+x8R&N%bb zKmB*V4qyVs05ZG=wLx%j8~`|QF#rheL8(DwSWm>sVW*?vWq=W7o5;3bA!3l(7A@cj zfj;&0W8wnfX>%ET_7Sa{Zl1iwLsOQiBWY;EQwti!+KbI8(rBVG8t@!a_Wm-+OlDx2 zkPs+=0aXW^rH~(@Cz8Mvp7!f?qpRa1u2FPX{9U!U))Cm}m%n;Z@OM$5y;qe<-flR7 zVQ0e2qLIC3uV#A`&}K0K=&oK!nOWMD_ZR_2hS|wUJeCg&BU1{3X3WlDgU^sMgCmfF zUMYwhUJieSLCgl=23P@ch!o1Q3>0(#oy3r*nh4bxP?ylREhQdANM`{iMOi;j_hqh3 zAw@^6ue#MPxP>cCM1)Z_8A#|>?EntELp)hwF1gW&B)nS$Q6df#DVr9^{`A*>2-DeI zmVz_L>^P&nV5k-clsH$aNP^LbY{^V){UohT5gneSxFNs}7!r>J?dTK+is5;r42?Z6>Wc*Q|td z@|3G~JV8#yrN=4+ihy3kKp-7u1w{r=S?WSBj5bmQX*0zjPNeC4@XumsMnEuv>f$dK z&nzsrie#0W;=sOHy~)$$CZfmYv5OQR4!mhZ0F41;U>P*FzYHS-#de^4@(Yxf?mV^> z&a?d{SNq7YFqrJ)*bp(K>@S!L(6Ixq-U+pB=Y+ORXEgqN*%q^xm+!yz66dk+yXC?o z51!-u*fy7aAA8a4_xa?PFMavmms8n$&7T~!_W5f8a2DWYhprvQp1s+-cmBZJXKnf} zd(9C5%XXRlnUqg{EnU3nQ3ucVe^UF-+ics7R$YIMH?L@anMVq~Stcxquq61*1DD7R zwxmLuQaF~q_hiAc^}D&ZEibP2?dh@2V2QWND|&JPzlW1WtLSORwb$p zXj@(IGHHowMYE~`p=7h>!rc}!fr98Q@m4*a1DcRBsH-BAyj4;_22eY_=C~=KYBL%u zzJUleRm~u>>_qc12u1);709Xfi!VIw{Buu+l)b#{V_)Am`}1X6%>Ga6 z+vh8ABK!C=AWRsL!2w}H+DEVu^J>hrB=qosL7;vMkgWl6C%%>}oO8nGKLrc>uPE;? z!^=Ri(6+K2X8~BY-3(Ae(B4*d3_HB+Y__t$ zAo~cI{bgVJa<#uKMY`u>CI>45UyEyDi4^Av;H(zR@pQ}az!r1z&_eBkctgV=1i$8zcnyZvtcFz59t2i2Nk3@5tF87u(0)K^fA z)DPWA#84vH6v0Z39An-N0+L8nmJPX}>Y@%Bqiox)w%Ts%t$puH?*-D05#xxBT(JjeO5(Np# z!6ROHxuenEq~QJ1ogxD5?6Rr-ZEtxChMpu~P6gV@qLlxWGL85Ow~9;=-g9InsEy)8 zWgjwE5IuvCK{}vE0JDl6C=cAY)&UkO6w*s^#M)7f8I8cF9+7to) z($Om@h>V~{A=z-QW6NDeWuBqe==f~BME-+^|6B;(0QpmxM0WERpYQgLEXEny_q%Bk$ui7dyAJ%pi=4y+cGGzHT%fj4oi7ghw7)^KW3 zm!aWSCqbze&sUwbw(uF7d>%cAqMo~2O^9Up=t71MZ0-!0Cs8WJLAO^NA6N7(JzC1G zDqI^)#)1*8UX)6SkWek8A}ulnJvKKB8BP9R0%o*KQ&V@9%Rbc9loZsN1$^AbTm(aj za-|FC*-+mzFj>j1ac9}}*=kRiJsgwR!!bQ}t<_g;25{Een-l_8#KVU%A#1m0S_T2S zX>H z{Y|e^1y&*r9DP-Fy0I!`F_8$23=TV$EkO4~h59>KP8Y%_Klo2az@I7DYO)b*tl8>v z$lw1v`_jVH|8p@8d$Llvn0V|7Cey;h2SQ4bWoKC=n~P(mR1MMACj$kAT#E#Ax`j~8 z5@rv94cW~Ap*S0|Tc$H|rBhrF(GPV$5o5}Axq+3M+tG-)WgN^s)`PZG2k(S#L1k%Gq6?YqlG^&Vcu!+%&#{S*aAFexjfB-QaT`>f2T^pVR(`~e1 z?I=!QUxmIUG+n6$w0LUDEdt}a(`j(N8{hG4~k1eGzOvn@wml#MaUDX1~-44o9 zQfEciI9EL8;?qImD+|h91}~!-WfjkEwSKHhnJ%i7Dvcg7Wi2RCl1j>S1&5sK!m}Vg z^u;GpWofM9uGNBVVN|+Ix>6}6Qtjvi=2E6}_LRULG}lOX)d>X=GnfhAjNvLs5!~v} zF9-iB{v5FEU!~ej2A}OKNpupst!MDrhLj)@UY-EqC+-7~$F{N$f7x7aPuUfwmJ3$@ z{F%FfW#QhNFM9gHYwa(8`q26Im*;G^j?j7RPGT>f`~D>hHnwXFCVN8}7WS2FTgd>j zJ!Rkf+HH&X*k!iGEYYcK`_2w#AF^QMN!VkP!}s3q__%z?hl?)f}#+}SW5 zCTnzhobTlHwZDrJzWjwxAA9tX-ctsaZ7YYA1ItpJ$97cv)DtKFw(Osz7SG>t=>dEA z3+CgFIo!eQv%dbNOE3Dy<(Hi8U!|_Q;#WNMQIil5V)0YDQOk3jua}6p+&?Mv8(mMEuL3EfD<7DF^FLjXKg2cfH%@QEag@Cxb! z+1XJIT@ga$UwL;xrF>VuH-FWLAdH~B6vRXzhIcCrA8b`ho~$8)VJ?I?MHFFAx$OuW zn3R&jgH2k(W6spXHg@o2F{8#XG>8z}k0xi8*57#4q_qz&mw27HW(p>P!Fk{&4MP}I z@jQwt7=iF~S1-pHf%y#(@X{i4X%_;>#gJ(6qKc-AsB z1T@UT&!G;n5o5^a`Xo`sabR#9WMLxEQ9(3nPZ9(u>sQH8+aXpQbDhC#tS*#N5?c`l zm#h1!juB{0M?8_CQH3r?ym~KsS>$JGoK=Jdw^C#gQtzT&xd@6744D^+ZJqe64ZO^a zaVC&E$~ocH!fpMjmjnCsyjk#LCOgMk^Vl$THFeO;e?`00zmaQV{`8q>J5cDp+$J$FhJD zlBgK!JnY6eG!h}0bO9f^m@on%F{fz(rJxE-NG_2uoHXm8;b!O8(T@15m*p*^Oy|k7 zC;9pbZq#8qvdOKtV5lof@i4(L80#$lUSZZi0_Js88_-r>J^)=jyCxemW(PH*^qPhV z!9g_-u2~O5<^scUz^yl|scGs^kOM;!SnS{b#h=dm&}@*|7V}DPc+GdechO(}&0pFQ zEkJW{DEJH-3*ruA+tRgLDhoaZ>%z;hpFFKFS8y%W})-22vMi$etXPunzLQMc>`txG{#AJ(^ zFc5vaw034e&H||jLj@Y?sFn$)V>dlei>nS>2GleSVQtDXwXz*#px&WtV40k7F|lnf z-*T((U{6#dxrkHcKAkO>474u5%L1cy5}VxsX9ko@Fj=G`Fc9tG1IR2(b3oIcS0G?j zDF8K^9$!$+uCP6F(*ke-S-6@Ij7c!Wt%(BIf%Qxg@S~}Nm6+6n9W$7bBF>~Dx;G2! z>NHJX*6;+3HI^F?kDv(q*Yn4@<>VQ#hzukND}}9KAIJ!qFwho)iiC`&gQ;jR>2$G; zU@sS2S)H2pFUby3BS24g5k_DWl@hEGYH$%a?Q|sZx)Uea1TvXvAAiVd?kK7AJg)Yv zo$4|DdTj&S@E=V#|W>(lm7j${VF#8NpwogU8MeRordw@K*elR|F{8 zPzX^*Lt6EcXS!{pL9k3##3-pi5n>c7TVe{9GL%(tR5#+KOxLcISEffJeMp?FXT;L`ny!-|)KMmM+ZNc{}iP0w>Qp1042}@*WH3`E;j0Vmt5bGwn6c z-g)x>!mt0G-`e>7>wC4?qo*CW-Rh06n|$b!7%(^|!Rcu4c3HvzWa!xFGZ+PNoFzh< z5HSgP0*-CNxmHzMw0vNE{C}(2+xZZD`r@U1Va^FMyK+`uG<<%K1z=UoK>$yl?enUd zYv<4!huHx$E{6vQV6}ABx#fQI(%-bwYuvIJk%IHw_;b!a@e7|j?68mR{i)wS$e%C6 z%jcha$|V>1$7)Bxj|Y~&{;& zU3?apeEpT*7Hl!!a@}{l!tDFlPC;-5P4zh#COw{v6~Rnsm~a@(;ciPYA99DxljeT| z$I2*f7Bz*UG_Q(}fT6P1Ob4Zcsb&%H1785LL)X&-m@L?69-q#BC0?dJ6_VdoaLeo97zpemMWro# zG@Y)mSx9bRM7l`61UX5S;SqnIQNg%#J% zvW_{$)ria$XIZMY#LKnP;jAKjNRE#z1X|xMdTyZ?74=DP0;<;-@eIcUC|XNb1u{KB z@hRI*9=~w5s}$R;q0-vU#EY`6D@cv*9Q)V5<~4y_XD{h!6NjjZV8R>05MDMvXkv4e zQEYZ135d;OA)%lm@({zc1{!z=*%ZOx`i2_ya0GUrna&65J(Y5pggksaW$SEIHgB-1 zDj1pTs}upyaFRW3<1=r#!3LaXl`?YXs_$he0%$~5C45@ri9a=2i6U-Qs=B%~byP$! z^s<;;jj~u^w7%lnPOc!`RV%AVkWPjeipWAtU`*5{p5xkRszy=?3aU|c466f>O&mQ* zGPo=LuIj>>Fcn9GGlQT{W+`q{;9)XL_SZm-VgjI4Fa8K9 z(tyHHNO&|N#Lxs7DwqJeE6!vl(m59M9!oe_< zs0wVABD_E^ajUAUKrp=&v0y+UFc>}KH3oSF7Hby%KmYR|N$yEk-2)9rt7gS5wv0v# zmeg>rqvvEjxdLLh`XmWSWjtDQ4N3A>P`c+@yahYb`p8tRrCiY<@F<%Z9ZK=|ou&c{*VfvCQYHjTvl#$Pt}UQ!%!%a8$>y6( zKG*398~Nq3fInPzlG!J})_Lz5zLNdDOE3PjKmTJ8&weO$7EE?60>MFZ(t$g{ekjBz zC7=obLW1yer;edKw_v7|%&u^y&tbvK4lM)D5Hx`Kt3U8RES|0F!c5ol%q!2~IL^B3xL_!-P3JW0es=HCD9;It7%z7KwZ+8!OJ$0DPoJRAEUE5cmxGO%oS8(@cj znMD?(5tW3};bl36)J`PoyCz9^lRR#TDu@V5xuT?>rkL~}YcGzrHczLHSR`oYhUS3`}dwrYK zj%UNmwwML`%f658dtV=4vL(EH@Zv2GU9!{eJI(yf`^$D+!+lU;Tesm*aZ5QI3LSgs zs`J~gF%~$V&LZZe+k}NrtzuS8D?UnbS%NJXQ-@*Vv6cUmT4#;-K+OiD&1GB6+ip4e z+?KG?YhLZ2q>NL5+AThw(<Iqa5TfB4EUSJeB{LWl; zk{Mp+18_FH4w+L85#xZqT{&)p4JOMR8IdXIVLk=f!E^^o<6|L)h&c>kG8%{#e)lln zs!C3<8aHRQ!Ej{k^X&^xIqAgD*kAtqF(3Dd>{Gt_`LBHW=;M$3)S0K*BA;w7pLyCz z&Tq4f=MVHHB#r2ZEddrUoF_sl5f9Ss?8{Y)ul2Sm051!6knJoxjP1m=)7SodS)}iM zg_`@`m-u(Tea4xm9P9t2;N@?9^Q!`|Y+G6SRo^?;E6ninwU=K2I6IQAv@b4*#{@U@CR?}SI0z!G(WSV`9qYv) zkXXPFhXkk=#RZAP5J6Ba;-d;Fh%vW#(Tk4UKg%3MJc6^?UT5x$*?sKGN$jx!-FXsa zEdT zkp;x)>p7KPt#`ywP*5*X&5bCN zO|D>+sZQrqoj%Mr&UK`tT2g^o^2D&!qcTpWkR>3d3+B3tT`jk2SEHv@h9t+>;L_oZ zDNbyQFD(RYRpDx4w=}mwY_71kB^?UZQ3>#>d02#b7eiIFc$$JEFen@ zN}-5`lfs^?K_1-_y*RewBC?2@-IztM7Gw$NrQlXKP>Vi>zMfVsh|%DqI+Ei)@n3T7MY+(fk7fB8%jdzeyo8$ z1x*@Q5sY|j=!t|*ND`a{Wo1<_0(Mgc&a%*8E|M51<)UF9Jk^DavX~2U5o=a7FPYRn z*kcbHLR@10OfC)oBg0wJsEdhPiLuocsbfFXGgZWLaljLElDK7C!VpiQg5Hp_Ogk0qCZ&@Uj|H5$G8QnjR+5vz zzA8pk3dWq}GYfFd^&+!oQXQW7sGSOE(t@F+t2aA~V?|{JNlQZC0927@$oMl6&ag0U zUf(dxI|XMl0bsqs?Q8btCCpWd7!x?!V%@VibAqz8U>OjbJtwpQ zUhoY}gVzASFd+B_8Vf+5{pJ4Q2t8y6^g+h~=gA(fWy=I&O_>13r{HBrnjK>XErCst z69}~@3o=Vb@buInXCDE(@uq9Yrg7;)W#So(8eO2fo-pi>KyV8w!<(uDND1I=Wx}YJ zRLKLJN^ylop-z_3e{fS4e{G}@wRO)4^&)L8U8~{m3h4vF3p*m@=zOmpzD31sERiZ{lU-%u9>{`BPz} zQ&UJ<0tiu-zM|_;7T|@N+hxW%4!_|#y&`1cWr?gh-L~`Gv=CKJGJn&s=Avowr=wd2Ii`>`1nkm%YNgVAmPmUmhFEFfzyt zFN@f5b{KnynJaI<;oBU^?l5-OJ$G2^ust?Ca^GzZ-E(XICw0U}X2Hwrt^TIpu)hp1 zduj?uS|&{Xz#2ajq7>)8dg8jqd){T8wU2BOv1Ma>lt1aMNlUynq5wkLX&Sd8-XnG* z`u7h%^niVrtp4tIz{N(57nywl%y?Whe=Zu^)@#1!U0z`}Eqw8qqx@4U4_11s52w_M zOaSs&w)how%%f~i5u`mPn_6 z{fl2a`Lo9#_etUFr+jhhkEM<|>eEY4Kh*yLj!Ck*Fgyqj$A7+w}^DEsqe=|bQ5a$0-rGM|0g@y=i`n6nMY>?F2N ze))ST8_hnE4LHNgH(z@R$n5H0r5<}=vIS`kHUC@VtbOLd=I|zSDS0?%NM22|rHNhK zJg)w?XXq#CT$#R51)Rs=%H?ZL+j3p$2T<7ND}ue`FY z4K#`?MOnqEDZ)$9S3@fnGC_^5$j|^v^<^b&<6=Vu~dJ-XOcu{*;jRHHC&&K#0J znwmq6tWia#Q#vXoNM|b|2@K^*W~?$DN=_-~N+L=%ax+$W+nbF^J3-!pGy|B0xUzsM z*OXz&NjAj#p65=);6n3Y4PhR;S2LIzPm*wP5%7KuPX|SaAsq)YXa%`ETgBIV@7fN@ zDCpsvT4b3|QgEF$*RXAE@==}IY06>tfWq4urX351!NOwHOhMQBE$s-&;9MrQ9hM=3 zhLBYtBtyx?UtC!&BJQdWC>R0bAgkxZs?N-w8nMoXA!3F#5JY4NaZ5bRlMOdP9M49L zc*M8~Nmz@=Q!XZYApR8;L8jqygGK5~#AAf6iCr8qLz%~18G(+-FeE%HSynu0WK11^ zBIy`G3ZmE3(Br9UQjxqRJ4s|whs>3gg@DB}6t?8ou?0_~!prS(@MpLvzX31fk0%SL zR29^BOIiJ}*36isO)3#78P-+2AXk=vVK$WRR=gu3hLu{32v=25!LVW+BhEuVR`-)Q88qEZQ-^n)mV~_h-Il%sQK_$ zP${{Ed^Qiw480x**d@1t&WmFT9H?enIWhPo&b?Sk&83!Xt302?nvRK{=BmjY9bbEm zH9SAJ$Xc?=Y|hlX_zz5E=CKz*9RSP;dD-Qccwt!te3l4x_K9py z`K%qZZj5M6?cRpq=vs2MOASX1bpethAT#(0GLs)BEs#NgK%r5)w+!X+m+mA|pV61Z zO%#oNO{OO^DFO6$U|(=(9BS@kUm!o=PjY`LWux5wxDSGn-%HI17(qp>QN4`dX@lEN zRs+k@Su6>e6anG;2QCFe_q4z#4hqWk1cP=k_1O0`Cj?HR3E(2+0(ZC#Uj#99071WP zNK#oRG0_>h>|peX{xWL>OoYfJ&Iq z5bWb&@kn8%Ndu)5%0HnAj%+YQP{rNKC$)h3?;^T@rNpO}v!Vqs6Q>UqGW=}h%l5M$H z0;>fd$FS`(qtT{=&oU9YQV6H2k zN+%(+YlxV~Ulk*~GCgjJPnR=##Idau!3LSGqG~c%s`QBpFMsb#7uBZbObxB;iWGM% zT}r_>zVski7-|;8dzTq-Zm+p?@s}|5j~)&qPshc!m7UYJ?+iHi_fk^YgYL~`N3xyA ze&p^;ee&zrPcN~#?Bm#b%$d2@hc@10&inoCvj3B^zr63FO>7zWWo&=J3@P__%pyMg zWs7;2558mmHf!2n7J9FFuN~L^==_cBFCVeb_DAl&v;F0{+phl`@G{70o#iW9JH+*N zA&d#BN#w6O&uo#h?}-2;1WQ54Agy)EPN;>+s_n6ouTr!`OzW;JaNLuD>oyft$)nfR0V(t`I#e4^5~YSQaFbO^vSL zrz~DBoJ&Xo_x>XpoUcY{`~trqKTr&ZS}3V(_bLKHS|u&|^kN6PsLS%|nLxsjga?AT zC0Ios7tkF-fxeyEQrs%Z-{wNwdOP+Dhr`QXJNa{7UY`0hsWZNL)=9@4eVCV*&pFHg zTfxN_vM^8#8spEltPP1sB&0;kJqeu|vv=<{HUbb>TtFb#ZEU+O#T92$`pYQTBJLEr9o0$jszn>k zz%qgmw6~k5!emFx($A}G=_dm|d?)E51%8Msl0m%$a1{tM3`l0_yp-&wq4gr&70Akm z#1J3+Rpcrzf}T6dd6SDFS4Te)KuZsZtW%sxEl4}e6)BFG$1-q@Tzm%e&>6sDl}9O; zNNU7EL@nH7#bHvN(1xrBW!@%W1lvh8;uWAVkt#xK^QvYQp=pYkTCtLXOv5ot{571! z%t%gIWbkgFf$3opbvU2)%M_RExfv%myi9$>hP)#vgmEy#9uwI(@EXE1Qd&^ZcsEY5 z@}S8H5YvT3+7aV_|MNc>Vg?ku*=$l4Fd?u5oDP#1l@6BlJP?g(#2Nuym?As|q^4We zqlM~RYt}5L8{@AcR;`&v!_gG6TF4--6hpBFD@jyJ=OH1;p!H`weB1I{IVCXTg!{aY8gQ$gsIJhmOlp2T3 zRqd+wp}q&RJX*m<5-&8D*mXtw%gPF=QxzDBkTb1i(Si&jRXt=Nq2Lxhg08e93|(l4 zv%adc>F3rXS?KFd=yiMjM}L#0X{uCnBg0d5A_7T_YXuQlC~d!@*X?b&TDA;ZRUq@k z)srP)&?>`kCxe2hEVBfj zhOp;Ov`{Yvu}vc~s28Q;YQe4kraEzyQa5E~M zzXE|hEATn5zPn{ad#nx25{Y2yqJpPh#A6n@1~nJO-*0gzAlwD>X7$%p61fT%Tn1DZ zcfwXOjqGuoqE*2sp-3YOTfJ9L=t%{~z-)uLfD8b~* zF8_{iTnk_tj0U^4*9qLYmFN_-TycmGKK;W#`UM)u4)l{K0wltQNqMum$S=;Qb6~rwiY~>^~ z*zTW;Z8WPL#soH@QGsCo$e3*6xdE5$t6jxdln$Vbz)^rr1a!&}2FA@FRvz zd<8W@<0ZR!av_+h2sW@h7$~Scm_wfM+Q7@w$s8b(HIegW%q1SZ^oj#hn41rQSpj7B z3bSiq(-kX73Wo`}o5E9+6-?IxMJOxcsz*6QL|HDkfiH0E?JPUs9ptyatS%a<6vHG* z4A->iR$M?(=(enoX)v?&4zLnwT$_Ucr%$Oes~1lo)iS zyW5dFWRr-1&6godW~{0=ZiwTVB}dluTt(!9;TU34Q;IBum!+VbgsG2xnH+?g*i5U5 z^-n>rpsgjo96b1XDYTmmFAKhjZM(S>+OBT-)qL#UO%5#EYwn+mrr5j38@8V+Ex*L*gZF$29R*Ro>YRWj_s3QC!hGmtFO2OA5t-tFKh$dkhm3}0X?RM2-s6D z*}NZZ)1kGD8=z7S!#%fM=iinA;Z9$J%pwqUU;0`GEKlBUKIdyE9{b7NciYzYzI^5j zWbV(TfMuu|n)cx@`^$ITa)rNO_SG*Q2nWM*)^we(*up1nN;i&7j3R=FP>LnEC9<`+ z^u}ETct#V!SYK3RfoN2sH08>2>*^Ly^wlVCsZ9*-ncQv_F`}hgj=5gQ)I~f6F?TJF zK{CTH6rMDnj;H-n{b)^M)@&oe<)Luoh zSd}lq^K?b;N*IY87`b4YB6_md6?MOSNV$ZhiwFftD}ujT1k5GskUm!b_LbkF8}nTq zd$6xzX0z-nizkm)UHZ1sL}10$i&cCu5#jGyBt4elAn~8cgFsdmcE!P?A-&N{^fqKU z$e*JMKd4MUV}RZ3sC^{ATF zZ6OYd3#vw=Y5}*jP#0?tGFgO|E2!E)(0hYa#E^xpf&^AXoD7PvkEFlX1ihFKj2q-Q~A{sj4jE}~x?~y?kFo;Yk%OU`1kXl4q3XLGQomd`DqB^(Lt`ib~`o@Tdd+&b7%GN$hhN&P06{)!mG(~92MCfZ} zqcU63i}0?9kx_>LdLTvcL7cvtk{Hc1lrmJoQHRN~RhA@*2-M*l80jWJlZI#e?_9+u z`=VwZX2Dz)Zq<&P&3AvEZtwWbZ&=CH;hhTyRQUNAiW}@UskI0QtY?}=r0e8F^>q<$ z!2kHizhe+7K(u}<-GtDeX#3=XIm#c$6T{?VHlDHG+J}AokdGg}^gsUPpF@kS_oz0{uCvEzx$T{m@`3hzBGB+MsmrHOqqdF#Pqu^^ZU(ugeE`Dca_A3+Ge9KW`8Qq)N_d%&Uc2^{ zZ}8JjH;f5y+NcGb-I7f_$q8wp(Ng*nqZtBSHK=CN;zqZAtCgi|5go(MIIs)|1Iy4c zyzEK~*FZFNY%bHWU1o|0Y#9nhW~E+C_86h_?QB9qfmGpQh603H9^Ma41I}3w6HuCA zNgijg(cA$t+vaO>05>?OuS7H+vIz~w<_Ag%`UCUo9-cY4%F^-JmWB0O^vexQp&$Z% zm7$uln9G|Zf|*JHGlTP{Ch`nK9LNeZnZZN=AJ}#RZ>Ny{Adn>vPYrwoM%jKfs(p+L zu!U;X;-hE09As)yB#?#DEm~t-+X)mgLbHHALw;FQrUHF+9{tLBY@D6$hQvetRmXHH z4H&8db8$gkRYwIv2K^8WfhRId>5hzo)LBtL1{EShkQV+t3!mkNs!I~Wj2a0bYt$%? z8-j$yD&`eYDn8POkrO!16r}ZZ#T8$MG7d2;cL-2}#xSz%gKT<`I09l>*;$b|h-ifo z9|oE5eE9*0sj~KM64vg58Ap$h{PdqcUc&3H z$@jtb-1@`2Zsh-@X1;gj6`jWh4?UEIH{msLgW6PS^^s`81RJ3_A;j8FE}#m-N_39V z;b(ib)?9lxoK<+%>EfQ>T6fy#<)-=zCwLq~SR0KQm7njKFt&oh1u1|D#8Ao;TEuR7 zu*fWAA{KfDAz~F(od{_amxZ&(p>n0bG9WFqZpj1i&04A>1J2xIs{>H)%J21RwnTv& zOfa>pmn1kNATuTc0$zuu1Rr3Q;!-?Bc7>xe6baQ!Fy~`gfF#K9Bpai7ZAx5VWZPu; zJ1pl7O^y&g3`kDf%8rUT>y;_&d2tT_2TL}kz-ra;*sr+cJfHd6Zu6Nwj(z#X=Zar; z(K!&bEoP|MF>LQF`}~(ghqV2x)cI$faO`K7?!RRA319g51!sTd%u~K_k^i?k_Z0if z-e11v(hDxT=xoF{UiF=OZ@>QGdwm@BMt~7Wgd&B>6QSq%c$4p9xz*u4Eb^qt=xhE+iS*F(hBt~#pW zPdgP^AKmg2t&(oVi6qM+qRT1Emk<{?T?M)1api0n3m*9JE@Tlnl`-LFs7`0jKA3}A zzP<(4=|wt*4H2kN+!G-)8!^;t!5xl@RaiXnFqai8LdxRwhJC~}A9)0gA{Q|odG&U1 zjA-4`aAQNd*XT8YQse|}aIQ5K)O(qN8&D5%%~?{lh1Gzvs_OY_p()A^F?v)5l7vzXz-lEh7-8of)yVFH$dbw-?90#H2bRTnX^Vj>M)jhKs=8C)%wo@skc!@%*i0Sl$N z^cH8ENunSDpjzvuWy?ZuomDSLi#fprOvI@mPAXlCDQN8$AGMQ;BC>EJSjvhcPzC&Nl)QCJf|bcwSswx zKVTAo&FRhvv}1W{V(!)?W>N51uZfHAVgKD15^|4SDF}s88E|mC!AFqb2zFo)i5w0YKdw670aXxRD4>-s)`dy>|u= z0Y*4Tab_nEM;f1w?_O-H@xHt72t|h75U857%@yaC zXIehm3X(^bUk(2uT%=6lrDl;5oA)bHq+4>rSvch8YTridvwPBltQmpVIm$B5|Je+ z>snc*l5H6fgg8@352Qq?-K7Uo1~y?UWYUR*b9T_6Oiff`7jzX;sa=zmj;FHZS1K)F zH^2~o_Q6M_+L37{x2gy?DT0q$h^mxR%4A?Mx9s4Cci4M#glEZ#V1>3C~~Sua67;tu~jXrKw7s5+{zL# zp$=Yg6-^#%;DY zf0`QwiKTO{^Z`?D={|oA5rmO}0?@cq+2a6qDH4$({*zxm|KlegeEN~QfAaK$zxweH zfA*tCUU>4pA3t^fi_hNw)FZb&`}pmD@Z!V2{P6=neDH>c?z!s8hi~$wuRna@et*9F z^n=$wasO36xb5Q49JXNd_rJ}{%XXO;?6RKy<-oF!W7}Vzx6QgA+IGf=cir%yMO%Gz z@izO--)zZlGxwgm(f+${vShan7x^rhk7@6^&WC5O>$>|+YwoqvYKQFpzI}FE_n_T3 zUb@$opFC*Rq5I5Q`(3a57rgu`zgIg}S%B0mG?s~wogNM|LU3>&#OIa|i2wjZW6hOi zGuo}?SR78|vhtsSP4u9s{ZR05;|({od1^`8Yw==l{Hew8u`VN49GL~$>q4LbfFND% zP@#16&Zk+s(F4oU(O8K{rBd>UBO?q}Dg|I=N#uJ=#En~JrCa4~Zw1@&1poQu_L98a zNibyv^s-3uv5$Uq>A{mPRO3Sn>2&l!0ItF9BYQ9L#cqGnYVG4%5XQl2iXVKy0dJn# zH+T4roRi0B-#%AGYGl=IK1rE z$G!v+27#L8icp!|s1}M~n_~>BJ9FrL0b4ElNsn|uO90UB2KU$wiy{hQ!kbkENdz2HoNRfy z<5rD)PEw&E58Kj+t?-dzhBP_^-u0v=b^nd#A|HTronW&>8G6N26oLU*}(h*oB8R|TaDh}jLX6jyOUydmH^j9k0a zC}>owxFYo;X2D9FRaGH~L__Pes|ipl9`F>=uP{*&8dp{0gHlBd<(8(xq)i<%^lIMS zw{gWez<{&B4F;L{=R9PAKP)~0r&B`%XMy5_Ep@OZL*s>n@n^aWg@C36ki}5sRMxmd zARRa2>63`V3-*#m2p#jr0}KvcS@O9U&5t6E%d_qfJm8INSO5&wj~)*1xj%6i)Ky5D)VL_>p| z1dB5<#Ia%xx#}e+$yFhXahTNTuozh=Ej@_w*VVBVM@eQ6=yYRCrwL6Tkdj^m8+B2N z+%{Jsj;7E*Zef*Ir%ag~Q zg47K1=*Qrj;8vobA0y!XiZjCGLBLJdL#a#={^FCPQ3O~9U~#A*4m~LfGEauv)ZaRL zmSqgO1x`KPvMcS;mO&~2dV=B%u0~g;hS5~&Lr>l_#atreYag6cXq4!=t=2Ad+rt{fou@>%!eA(#*f{qD zfNbg|XJm=JqI%Ixm!zsA79Y1#3fi%yL_?d<%%7%uewXGJwEIXe3mgCqU;$T1!B!f` z04elVBez+^Z7Ce-e*;Hwr+;|gPoI7GmoGg2;xi9D|HQrU@^eqz{q&=^KmX)izj)z+ z=byau!S7%B@V!^R@YG$8e*X#|`?}-W^PYS3=4T$d?yl?4JK~_(KKAARq;}i>z4LdT zv2fM~PGV2K^tH#P2ky1yg6-D_nHOw7W67M2_nG^_y=HH;*PIPMGXH}g+HS2mTdlV1 z=I@-f#k=Nh_nw72ueE5GwO#kwb_X zq}Bo$5Lkl;fje<1Pn?(NRs;=bY}o~1!9c;DsashvtboiSB@ru_(Hd=^)Olm;5#Js{ z#8A4R2r^g56b*;jCXT8hSxb!Kf`MjPS42hJR2&A-R*}tFd{l=Zq|7T2+MedpD+|>+ z&W)8lU*h55p)~hcRtQ>g^&$pN~X9np+49K*ua^=h^`?SzZYz34G04 zaQj*xcY5`!7R}uWOoon+|J*0PxiSZ{`2e8{d4kNRGFJ?o^58P+V)n4#qSKU{HGXe5`{ zXYK+pJ*ctSp-bB~b%5DGb8geO*1Zt`M_Z?GBPu3UAuc2XKB^{(fFVi9)2(!ssw-2Y z=W~YmNX0321kq|Z2r_j>PDdOB43UAT98?AAsP@rziko|-sG4}lbbFC%@42P8!3!yS zro^B3u~}cVhStf`QO}HTX!v3tG_49RQ)9x5RyHK43WLtBlQGVPR*?u;%>p)zjAFhq zw1O22BTgiqHo-I;KiOdSs0v#G6O6vH*b2r}kw~`~5`gJkv$+&OF7^4fp0Ra@zf~r7FU{#1O8}6c zg*C3YfKo-&R0|rc6cNaP16C->3@>}L(&0=IJ4YOHNVy2d*tSu~6kg^VPj8Y0A=C?ZQxBdPb{KWwSF5slb@{&_xjkP!)= zj9_{(^el_U3s!ugvf@t{(n=?A>PtzrL6EIRqAH}&lvHd|sNK=1nj0S~34DojdUITP zfE;ouvN+h%CqAJwz*8pxiR#RiHIWY>v5tW=o-AX`db(xn+~R9!`yLlvNFtpb2-7g6 zERoCr(g_KMD^(m#Oms!wj%7WSX{tI$zbyA;P`pM|wRXq0QKl?0_?9MNQ;Md~Ci<*z{ zL~RTj{vb1`ZgLNO#~1x@Qf< zBJ3klw?WCa@F^1w%VVf)JSED)!Jix{s}zkgjyV2U)n4L?$i=g^sNq~kVnSjbe`E#e z6+sy`ah)D8DIimOW+$gWJOcq2g|Ty07^hm1q= zXS%K|CNWb})@za-CvY$uPwA>AgDTWmb;iQSwV|V=InR|Zz{12XbbNR!N^)Bl!-gU;_^ETLT_n8MQ+W5ddKCs`Mbv`nC z?S(tM=a4-%Idt!>9mzgu@eUiU`+L6uFY^g`mB1Yb627xoK!xqJhW_AXuT{diHl3|U z7A$c^+Tn$--AZ>w2^d0!V5uSF>LD$jup|s90*aznqbkJtY|baEMeT~KR5~&R@rN`4 zVf1ROAkH?X`OokyX}fl-2pUDW=!%eyca0dvmqjoyV5+WzXKIAj)d-xD6R|3k!V`8T z9#(`YIP$6r@(_jtLsUT9S#&$UA`;tvQV}C0Lnc*3MdAq5g}EwxT$hf-umr=uw&36W zj>+eS*p}adUKM2XAYG!M7V@hFZ1-B$DsOqqqPcUtvkc+dQ~vT7K5ci{e@i)-4KF)` zZCkmof%)W@6nOc|Up##20Sk`*{9*9&8DIOn_m}N2yLxT;`?p?w_bpdJ++K@@mmj@< z@^`~}yMFGf=l8G(Mus9gqAbfN;$3aUPCZ0=$)D#AahA9-90{IAKu_E{FOlo=lD{vGH#RGhm~h=0zQbUb5=Qos#?pz3Z>iWvU5i6c--NDNsC1}RUSD!1L^ z<>HtAUJ6DQx62GN1I|n_9u(luvl5aaEpQVgh7R(n;*UT?DM1l5ECcEAwYasMcI!(b zxuPVk;`mer0tH#Xt%xj$m6NZt>9h?oI8iW_`Oqr^+MW1^zyBMN7^+kiI135jT7&=z zoFwW}3#%$f43dkhOYI0`NhCJhM3T&lCSqC0#MLDi88N^y55!b~SeAgEp(^D{B)J%} ziQ=YZf<4)`VG%{}1nLP!RP=5U69yAx zZgl{;DBa2;k3_lg6p_H`wN~EoR%wz%u7t=+vbvcRg4ti_`89=BOPLg5jnX z{3%FH0b61O*)oXSlyya{AaychSu7)n6Oa2XdDCM7V-=&@Kyl#3x9#RtRby2ujLu81qox)v&h6}FRs^V1(2Ib3lru3)m`dGg=5 zbNtV;%9YN&RIjoMday8S=k1)Lv8-by#B;O=G00}HlU#3c>v>m$onG`6m|cfNAcd(t zf=fuZg1AIbH&d_-ZXOakUFQ2C9zsf2T!&OO{*0y~0b5p*r3INqx(V5JV&tiPbMS<+ zuJ8gi8G*;k0j9WHK}5(6hFnZEt`Rdr^lq6epjQ@pU0R0+Je_TJ1(`jz0#3b>AP%U? zwc+c7U52e^M~T7CIKTxt@c2lF96=kqv|$HVWKM=lSLqO25y7oe!wDCWsE8W$Ri+R$D2V11nh}2iMATD4imtYc0 z=^9PxW#J~I<4{vs;xPgRUDG|uhuKBYr=Y7qR6L19aalOv3@i&QfF9erP&#_lKu3_= zqms_Iy9z$i4{bnQWfgo$@_&t&4W&X4i+V<95-=z{n)A@f;e@+xyV+Vr7);`m zzSPMM(=C%V0&&u-)ODmptf1m_bjvst$G^54i8Vo$if7>%#>6$7$W<1l+e{4<*Te%L zkBD?Zb%<;Igu%I6qT)O%a!aXKCJq{uIE+w1tPl{1Y*KKLA`rthZgDPsxm8HSWO`7f zAgdy=8Z@S?8q~Z>OF^3oZd(*ai#MY}%Kc+hc$up#*jWRRrytKo6J!RKe(VD7AqW-jR0~C4u`?j zA`9NcRuQ+#!inKiiQ@34tL4%r5$1{`Gb9a6St2;21c9YPxT-^D%NVMah=v1?IVPC< zmNk)NB^5HoiLFL)S&51uCXADW&Rk)FKNYYg2K%51zevPTB5vr(qEbF2EN0!rTw;nK!!`wR#z8;95E%%WiKLr~ zl@j6cs#?6gR0R44&Lvu-Z4Z%wuPYtB{)4AlB*KFV<_Uv@@H=8;B3xpuMrUg|P(;1Z zM8@PN?xB7(BR&g@Af^$Kbcm#d1JK&3!*sGPYlM8GoC7gQ<*P14d=5La9+$mmkl;z_6$ z9MEupJAggeJNBxzKSy>*VCo=fXRtx$Ja%|_a%Ow#gKt_{jTFpK_)}1I0`ctVv2X@S zS-KEF)yR~kAYHN~CYf3U4VODm038S%b+-}+QeGy#vV@^aHqq!hz=YM5wW%x&o3kgG zRqe`jdcJFol919#NO%m>PHw$jgbKLDSzI6hZp?xn2OY^l5un&72*5WFc67@uBiCbU zuUA+I^d;$3b|pYPJRy;}RU=k2fR1N#l`n*EyVmh}%3s9wT2jbi;j>E+uQI{r~;n zfA*~{$F7WP6>54N7zYy4n*Bj+;460R@p!YSr)ZZIEJxc070b` zAxv^2h@L!XXpv1Y7bH>&&ZR=k(*nwK>k)xm#j!=;aREH*c;$uXpL50cF7X<>9bv(# zQ0Mby=@r}yMN~AY{pEAH#$7>I7Yq$U$>dWob_@qo2fR=!oGb{nJ56h%)CSnRf zN{Osdh^(yIE*okHiLU5z?&t+gt6Zm!V^uXm-$SD~r3@V{$7Ilomul33%BSvy<4)V^3`^ zPw?`ykKGL{Kl}KdFFbWOy!_&`_khe#KYEL+lh|HihMFI`^HMJ_Kl||YpIkc6%gX}1 z3@rQPmp@+yndk4a?xHyxe01@4`!Cq?BOluIfZexnwZFV@rx}ZP-2h%*>`mt#*Pgr0 zYO}X`=dN44eb$!m5b^%!xo#VE)X57jJgJ!p)X0*=46KXRHV>L$Yu#dw8T`~I4wC}GNfewPhWH>~z!~U655z&BB6d-w3y9sqckm#XDXxes5G@M` z1!>_&DfWeB0g}oB?BJmQ!y*Gb6%T2Oz=>!?ly$`f+EpBXMck^+nM_%NIR3;AW6}$; z^^U-b;6P>tA>SfWU|aRdm7-F)AhU?F$>2EvH3>mk zkXaUKaZ?sUSLwq3`z~?N3kK)X%O#1e7N`V*_Wf|6UUir-3zx~^+xfG$v$O2;T*rO( z6X%`zRR^$rB75?l@);*X$~KoJI)>dJOW9~X?;FRTcJk4mK4RaKzVr!r`Rvn<^S8?v zop^$}Zci-&IWq289?=Rb0g%f*H)D=$jfJmReDprf&jB39?v-swS&-6G0;67prmlv6f~lGoF~pPbJaBBy z*$6BXvefHVz0#|62*bZpwSvebntWVQnxMF{ILqaRA>eUI%w1<35oJ|}AqP*yjs{`) zkN`zHs&<*N#Z$cqGz8fMW$~0H5obaQ<>DDC4x zag`SEiNES-gdqY35!|X#UGbNKk|XMtzKXkrN%3@ulwcxs_o5{LjzGPGCpF!oCzV7? zHe&qoBq4?hdIClT5i+1rwLN#Ec#=?1)>W2tijXS1DXWwqBAq2g6j#=5wIFtz z*%?ho!VN1ah(~*6A4O_(1R{bIlq7Mbh)CRAtGaX>$B=T~uLvi+BbRoOxaHCX@Yz+a ztn?y}id2C!NzyviB$AT=5>-JwfxrY{E(O`S2QSKIBDqA8OXU^e?CSg{-=R&=J(ppO0Rt_6TxCcC4hjTkPL(`(XFzAETSqdT`2@ol*LvR zQrwETqCu%3sYbiFEgj7;6xDE+HL8oP;v*zRSGkCB$evXvk@Ln0Yv=E>*#aoZ%y z4Hs|}-5Sw)2v(pgC&Xi9k5)_zS%Y|_Qe*lunrqN83SM-x(9ZUtL6XMeMdvrP?cby3GksHdTYet{$e@Vs<3k^R#jDggk^H zVM2)jTq*u^XF1Dj~G^61p zB5OcNS5O>Eg_lJ_b95B1)jSUYf$2+(P%vW9*q3vCP0n!wSC376>doPNZy)1>0Lp&J zAphzt?!SzEecmu^Tr?OPfW#v_6$mD--bAGxWo1b(5I)_>IZ_aeAQ2zA3JxQ~m4dGX z&{V4tn6%>AU)k8Y4nETpYZt^!HXy&{qU~m zo|r&puPs0K#2xUm?|rqi{KDh6-h1;!ci(XSi%;Hm*#5itK6ZGy{pGnku5EMKE6n>X z-fX}5Ti9RTcitukE}Wp|MLTUUZ`-xzZ@S=-{zGM6l!Of%bxVvmit$&z>^eXLA!G2J0N3 z20y_)iSQ;Irz{}p7E}c8f|<}OtP07Z2^gXjP$Gj0?Y{(3gTRRG*utvOs~0h#je{*$ z%%Nc6U%A$8=ge=g8JFlP-`bHW;V+S2WAc=kd;MX+K?S8P=(f3X>D#WTS?4$raSAYlmOr@dUE0eoq@?J zC4;Nt;Ti(C%?Jb*k)8nwEM&lO1@Yu267@+UWLpt+5t4S=0xKyDBDfv4rIA2W^;TA? zVNCKMMnJ;<&)I!IZCX_czDI3s)Ha}^tstW0P~=bol2eg0NR%WXf;oW-7*Gr&(jY2= zfFK!)Dk!KbLRCSLL2cVMck5^;yX||OxzlTU?wZy2_kMpl^R234)|$)OoZ4rf9nX2f z-tT#y=Znaz>C4R%nj2zQmu4<$d7E_;2nhy1%@_!VXo;tReu=jYBe{14vP{^va*tff z#&wm+F=*k`(#CE8=M`=Ab68xB<|n*$#DFa)t&WAWSVsK>VO?fPb z=%k21E?1gYJF?Ts=+y6=lsj$Rlzr~o>FipkOMhx}m42pe@0x1he8+G4JXe*wO}pp1M^nqF!;ccc^Z?AV}dds1@x4d>ZEi){C{QnV+Zb$j{(n-jq)sn8%> z7EMES2gGtruDlEH^(KjW$_8s5W~gE<>n$8NFa){8NorZ-4|D9gGT7Jlkcr>Tx+bXZ zouoW5n&;`!5^rW8x@<8GA6>T4dBO@#U2~iNCWlujrU*dDqJS!2f(1gYQFIiXX+VUi zm_$t>ZIyL+SRa}TACZP|LxZ6yq6g$dY&fR_ex9J@IUW?@2r93NdKit4$nF4AJfs0V zZ7k9WkDm`1@T=x1*A)~5kEg6XU@ubFg`Oqgz~n`v+iq4GI{>u|aP{bir|dg$Kkyv3|uzWe4&pImhF;rni}$=WaV$E188+xyE( zX7#dHnAOYPU!Hmxds9zhAHDBZK922q>;raQ*Z%VE+pM2{Bch`cA2+YEvnz+SGu`J;f|KXwOvPscuE5LB&_eS+$6$V)Zm~btMMnr4c%ORNTg?8L)E7SUVb5cGf7w^R6wc~ppU74wE0%jCTfOY1U7M{v zN}*bW^fS@l{QNI%J=|Nv&>agYgCpJ z54Poj2sY;kC9U*HwaM)5fJJx>DXl603sZ9X%X@|@v@D~U8ri)}>8E^`4-SeFc!UF$+< z?slV!k(ilSvK)p8kQH5XA_=r7geD5IiUm>9@UQhAE*R zgCcRvVLoV^E+N>w*5yCty2R$J{p`?(l0-ovJ^6;sdU0DDFv?t$AQlr9j)*}^u&GE1qL~B)@wY&UP>v_% z0^|wz_)G~vhimzX(|nRbr@=(AOCei^07MrN1OBF+IQ^uEAo3*Vhh}sG*t7V|I5 zmHOIcmiJ1GRi+iHa$xDPy(BM7_C~YX8C#xH)hwkMk=-KV63v`peNtac``L{M&lz># zb+~$h!l6#woEV)P{8N>(Qe7$zAXUv$bpGU9L@B!6H@2AO26JqgpVnpAMQo9=u5Z<~Mg|1_ah~u0%wdiJ*i6Sxt7YjcLA_90s%#IBa z@t+1oC{95+4Vk8A@Q2WlV{E~35;sd@(wvi4PAg$6*DV7qO&y6}so*4soT`HTghj2} zqUWL=FidldClD@iOqd|A46&|jCi&pkHVzo#L-Rlt=)~9J$AI7Dh=N*dm5IoDi!r(R_CBa*8dDKjqvr^kecSJzku`v;>e8mH;0cQy@Kmy1qlWWD)A?*}UR0@zPz$HMZc|#NefGmR>^an(C{sO;uzw=4>%ySqT@h;U>=4FN|!?OKyy|K2fr>orWfP` zV)e4P=){>ICAz7?!;R@8*p|1;C{8d&^M4R&Loow6U|9o8Il#5iQo19!8oNSTSrnr< zN4;!EsSPHGtz@8TXqT=1Wm&YLpa13KfBp2swv`{e=T;@Nr?Q`3dhg=-x7uI!C#8HJ zd*NNz-hRWSb8h*%_m@4Gz3A?1Zo29Vb8o!#uA46N{_^*i-r@aaAIJ9hm%*%^*7o<8 zdmh^svp*?y`m5r&2!buYC0dnQsqH>)WW;8HkVb+KD}!rRRyQeQ$Koa zTZIVmR|tLePWfB~rvOw?D+#f=-K#6z)Kr2v@k7+1c4jf?5@DqRRt<<^cbf)0Yytc# zM0x7aa8GOI}ePU4D9?Wk&5ad%@~Ju4hMIGLlIHf)1PEnNADlLiH>5*4%36`gRtgGc71_;k-z+jZD=qO=}4|sAiD|j%AQA;OIRj{Gv1Wu5LNojK)BG?fNTZ}fz zzV4+c_Q#~Wzx=6lKcHClJhr;HN3#3-%L?kr&hk{reEC-|xaiX#@I1EtWgq+c^e5iq zFE4-VYhUnvZ1u9|v2VTp3iYywvDM3}=G$+%)~<0ot!l=erf{g+TI~e{L+F3~!zX_B zV>!tmpSY7tIGdb75*4j;N)qDaa$0p=HCo<|4)6nr)39@JoR1Lsq|3G(rtBJL`7I?h zYs*3*nBicE4;(_jmIxmp)sk?YFsgOpi%WEV`vT5xr@Uv;#R$>0&#sg<2}YglW`It< z$=Qgfx%&cV3Z&bIAzj1~X^ss)%iAew9>$!Ylzz)a3*Qi;iKT&QL?R{!7}6R|4+cj0 zV+myAx?Sib%<}D$Tg790Th%FuvP#VPxr{Tq`NJg$g@8pOOc#F3EiQn9E^3G_PWtni zAbNLIZ8L+xjTO!rPByCo1Zkj+AY34z3pH`d4P^__nwBBsq|GF>3R+U}7vOM@2qog; zp`kc2)6l{hEnWznKdYoAPNVF5QG>tBrd9kS11gr`@{$*g`p*bH^UH0*fK;j zS)gkw=%g7DWCF1;6VXi@HURjPcLZ;TQLGz+tksRae4g|o#g{Md*fa$D=PRfoDH zy6L!7>I#at9am#BOO`GP2RsByV~+8PCjf#$P0?XLqGR!Y4~{F2;n#L7aY>iMCf3p0 z+*(;@map3h)07%@Uf_qcvc;||z-9y88Y%a@W#bQs17H2}dWGKvl*zo(y6pQC;U3j? z&MMzp$__0@0v^^86@4_D6S&dnR-XoxCLGZZkghh{+I1MS+vW4P+ zRH!xy+H47p8@gnzi{r%OCHzMOcu=fS>q6Xe(22p)kad7rR)B~{Al%mV8)i#@r&thq zVph$=FiJmF+#^M{OjS}^Mk???HxRo>0iHnY0B(Ltcj1umMyZYBsCKBUi`AzPr-PHu z`3N9J`zcB5)Nn?O=^c}wQ3_G`W`S%hDoZVs6goO5dCN&geLU({zxZhjgo44-u!uD4 zj_U*KDq=ra`Q6NqSD;5eMx|VV&nQ)tWQYbnC=)Vg6nzBFRW2FjTRb^W3TFTx-lAY@ zGQ|Z~cL2o{JxOVGDbzBm#Re#0nk-IR&nOV)a&>ru2=ZCs2B8p7$}`qY(!i zuPO#(zykqd*K&qtzG<5owp6}?V!craPD-?J4C6%0pQ1w~1t5506JT8?Qb;-Q7`B#R zRo>*G<*9Sr@gF#$P@HQni4J?Gq|?E9;dIb-0&+%-)Y1a1uiXAc2=q9T9 z)S?q_mN0iDU_jT>06~W<`E4%SG55B0B{M0WGipd6;!2!L1c{_Umj;|vWiC3NW|H9Y z5Py#09159mI)H3hj?Rl=E9)tr9#LUpix_`6Rip}M=rIJ+hKL6Pgt;v8kd?tREE?t2 z^y7_`!0`*SJEJwVg05gzx$Ek#3AIiGApv>jHm#+24~oV8_*W-gFgzZEC+A*WP{GwKrSuwX44D z`78g)P-(%0OkKvQEW!+|h@- zryu+F!}@*~DfOLuy0RJ@2Kfv6__?{*`iaIh{BUNX4()dY-ymU8l2nMh)`}+ zg$pT6f{p5$88{7yOrRt})v!fRf>H2u4AF$3^N_4rH5&{OjGrf*M5$QL8Okx6Ms>F@ zef3A9Y#%F%Rn2PTwwpbGt!i#tSq0rg+P?So=}(;Xu2T;4y)X6h1?QgCA7A!;Y|mrc zT=qQn?3=Ff3iE^a&Q>zpU$)cQFGX5KB7pclb}u!nB_S%us!QjQ456!NXI3XvZ6}=; zgNvtx1V506fMju&+YEZ6TKtqp*Txy8t2Bx(&03%3WdftBHh9@P5r9}X{)pN% z(-{sC6leyT=R8Ll4!{wy6{9Hfq%ipw=q6QOO@#i!hZyQY9VL-?vN+mY(h3YiWCg412 z<}HV;o_nNBqZ!jC8(V6P0{V3cigdW$5PFlK+nh(ihCdz-G^Ha<=2*cA5;RN1hv&^( z#EBkkQ+rhsV@WuWlgrZE$yGtyMUoho8{=V)eoFEo>rG%T83k}efQ_xZYNAYzS5dqV z?((j1#%VclItQs`J(u!IHqMi(1Bm*)N5r)pQ#BLGnQ_E6jXAc$)|{KzR%A>IpAj7r zJmD>8oMlcfbRp2=fSF#O79-~&`M?3PKKtw|j{XR^FIo95+Ec;mHC9Z5F#RTMe_1Jw zS@}lRGV!OwEN?M$LllEfEzs?VZJu41xRf9fIjvD$N-!smj-nMBehfr#0t`*mG7K5Q z3=Dw`aXv~;b|_LP2bPtMY_s%FE?1hTmH6q(-{jAdc~xq^9^|B{Q=-yoHuz(fcql+T ziemG_E^oDL;Zfmgc@>3AJfSLWA|;N}yT%qSwSJv&)EQ-qTI);Ds4i9Pa)1=#P|KW2 zLtG}_(%{97Uk{5e2CcLP2TidOh~c_46f8>R zbD9|f!%BAJ)aEt>Jv^N(5fzE@G2b7bPP4n5g9u8t~^#Y7-PfQ5Y?JzVXm#`Z4Q*>BunJP@0LGE08#gIi_o< zIT@x|;bbDjF=ryX!*mQ;iD78K+2zZcT0ziqYX{wmM|S{BfgeIACG&tuQ9Sj8Hp(cX zL+ni1T=oic^|H5^{Yk0nWiK!LxqdF+FBUSrR#UuS>WliPc5zy8)6ulDMfuH+9C zEnhDy#C%jrX{nBL8~t->HRB z@N3DcDDi+8Siy`(0jLs``52FDC*0 z7|3#iLPPP>&${}T9>A!OQM|=08Os6Xwx9R;Y!Vc+p2f;8m#89AZA&NRtP>Y+4iqw5 z#0!niN@H(K6E~|yi;=f96XCx$mk44gOoA9_V3B_2OrT#I{KY7BvAtszvDQ|y*7Mki z)1j()!tmJ-U2yK1>Sg=Op2t4_oOgfqi|2jiqEC1p``h36vgfgFG5b@_>gB}?=J@KD zy4X|4J(dhnCwfX*&3MmUxAc^dXq!ItM|?o5Uv2 zC5}#3S|aD9K{A6M9Z`!1bDX+xMwv?|=5%V|lP$1ml`JBK)Y@Q`+YkrkiV&%(pv35O z_L9X<|5|0_iA06T<{< zK(JwsU$;w>5V*|>1VAtp2*LPqiipl(CH+R5JyK>vIYt91-Ku7Kgpg@%;wZz(p|kmq zho95}U380;TFkltQWQ-EoI*SeH8W|*e+tv-;3+NdR*T4l=vrcJIQ>Gfu(f!aB@VNQ0W&{i(@k5e!y?2GAjKjIAWKBI zxC)cE(qOr++*{tOES35KT|@{u>lQeRd0v3% z>C6Y#RR=)2j>7av%iD1q?lCG};sBCMO$dB>;0cS40c$E2IKT!+A!xmLSn-aTPB@|= zN0-@Vk*(Tclm?vU*;@u4<$BZVuY37RU*s_8$RSRZC2;ZgDKMi{^@1%1b4Z**K8*S7 zFlH1mimipN)un;L0@S6=v7yVQVlj$21{8X*A&M7gny0sz|w;6qzoZ>gY5yKSfmFlMxmiU|3nEkCTPIS6I~|@aBO$tIF%gjPBFJ0XOOc1?gHpk z!se{uIT$|EKo78vd7ht~FBV<4bU{(%^H_JPh+{LFZzA z5sBnBJS1ndLp=I)g;e2p5wmbDs}}D1=GqGx(CXKEPgz#1K$av|DeEFuKFg)m$TpfsbQ*i+KtrpZxm%1(T(58Puz_42Hp*Yag-4`XW&+hemS*u<0A-d|QV`}@nP;3M{)p=9>4FI&vM z^mWJ{>u6QYhwrk+akJJvdY|f@K z=<`cBN_vu22GXGFtnNGU__rT+@O}zvTd2xRf)$v=;W*Wp=+)P%ML1&0m5PvxR>}>j zIuU8$JnKv-Gig>VbDol9v5_)V6x52>5G5R=ry7U8{%{$f{<5 z*lx1VeD$R--9NH@;hfKW^6WEDJ-m9^{<2CLeDSj%`}!ppe&<`4sF(fyWq*8Gu?*T= zu3%IoDj2oCf91_(C8UF26{%Rprd2yjDoQ(DCm|)V5{%9`X$)cvoqgB_5gm>q{h&v2 z=U08%j&8XJ26!S`S7u$xr9)2MQkO>Q35a7TUBM4`^_Kro<$I@i)X2I(24!8f zG(@DzXCSn8nFVgI*%d(SM*Sfz4p34=T})gzn4_@UL_j~;Y@|**J`0&bT{CNncIet8V zaw22SIR-dMiJ_qNijKL-Awnmj`>e|{B!vVq#AAB9h8Iyo*`kvO3aNAwf#`CNT|>lU zYiO9pfbJF#@StPXHi22ez%<87iFMh{v3z44Ot+{@Tl|?`X0&cI+0?!S*p=!)hJ~+BMS3!K^&0#D2*$R(MB%#a9A09AbRTbxQwfLx`- zr9cI32m>d%-Ic*8&~0@vm>Jub2Ols?MeLXAfBUchYDkiapP(btlQ6#bHDkkdSob|= zqvJGT2#*7@#YGRHmbcd3s9=e3!+`-%jB*|WD|jeDK}Uh}hkKw|LIL<`W(eQ}bfN%5 z0EMdQ#~vaIyVR1>!g+{iLIc2t&R=*!k*T)%ZyD#q^ z6R=M`ZVxVcGlr59MBB=h#^X?TkUJgJs?I?tGCw zv!(G^GCoAKWSmru@)m^~{2BFfJ~$=S%azPr0zA>>Cp`nGV!8=os2J(w*nsLLE{#!0 zWk|AGqmG|ISNY&T;-M#XJ(tTIZFo{bmc`;V|C5qoYBc?LI8YmcK1`z!52(!(tH~Ce z9zJLdaRWf8s>>)CL=M1>cI1;vo9?ii30`Cc-qZ7rM=-e46bqBewa|G@*G3qihwk??E8!J4aARB3Ac#AJvbqbP~Mj^CS;rGo-sh z7_wuO2oQ@y6x3xJjz>$IsmP_4m520XB?oZo<{Jc?*nv?)>4cy!<1k__HDhGOm=*Yd zAv-8Tr=iK{vmeB8eFLs(UAMIjp#(A5U>YT}s<~nran#Ed%d%?^X96F`{+@3LFP`_v z1OANEU5_ogd-BJm9=P*I&phyhCl>fPwvySGzC4T__|n%S3vckpm+zl*)qQtfVSo9A z!*|$jv)6eZd!L=x+jp0BC$Q@}2ko`tVf$`!#QvMvT<-hWie>xDKnd)vv*jiVXU}H$r=vVdtzSaR| z!s(}hX7B&@Vl@$^sAAa$F@U6*EPpCInM)yYL?(Ppj82~r@wpW<6WC-oA@0Q#s-TMk zKY+8V@=dLIfPCovXL*C0P8Mmy#t^rw$}#iVkW$wyVIX1%(bQo>X-^rEtggAjpOX=Z zU}hS$zpNbg3^x4hU%d$832h~F-}};qR7!hb`)e1U?|WZooP6j<-+z*qm(Tmynf{p6 zH!uCX_m_Pl+w<6W+<4Vpw_V@&z9#L$Icj7Fwzk)JD;0ZxdDP4O%_--C3`BP4@+jGG zsdN&#qK3PK%505-vJ{C#S(YJLN&uI-xW>vkr7N;cJWk0n8ABKVV&YU$Xmq-^l#~L+ z4Cfn901?XNHohD4FaPvgX%%yLRAIcDih)hpZS$P4dHO=`?Lk7v^Q?%}+}V zhiIDwb}?&75$`VSc!=0+FQY9hh?avY&LmFf3i;8q(k*~shUC4B63->7K(;tA);c=P zC__U=n+Nm`gE$*PVU!^zM5+q`5Yt%f5R@zqg(##D^HbN*@X!#onL|v!EoG{ z{!EhF?!W%?-;<)BTJE_z`PQ88W~-My1m#U<2Y^G=LDd1k7AOQRagP;k(cz&V&IdZR zI1y3kL16-p7-FFnp4zce{4~lOD|i5$EFwXw3M38mTa^6aOhv&Ta- zNswiB{LeADI-Yz$$EgnGD$eR<)p{G>os!PM9MI(z z9**(Kk6pz@khVe_(IEzSxKTdOG#oP+#FX%a4_aMO01UK2$cjik=m0}W2{yPu5^M>w zLNJ{ifYX2zZnXLZqKjy0XoKfZMjOhFI9UPs5piaeU5GBCV_r8C)0MEEGk5?z5AhJB z5QS7nQTpT5<`p8y3R!|$qr(jdC3wt$b#xG#M*iTeUPjTyM$pC0+3oaqcz4P>3xMOl z6Wre^aSNy?gZUMW`RWD;Rij&X$rv#_ZnjsqMcz{=Q zLzs&nCm%ZKg!M*GWepS7OvW6#=tjXh_svE{n2e#0s&t*~BR=>;pR z#guyrG4<&&M;-bnQ+v=VM}HL3pF32^+IUqwdUF}kjY%nosEAeoR?OO2MpQnl=_-LM z6?K7vv4@=1%d5Qn#iTsrWdBrQsL0hS4Edm?mMWg8C23aMx&yY^ib5xTZ0cp=DC%X! zz7JC~Q7KBBD1dGyC(Ssul=ICH96wH5>KSbg9&o)9hJ$pQz6P^KMiSB%Ie2{CQ>>23YJmzvcyA% zCKYsrbgntYWD72suBfi7kdZ3OPb4QUr-n!w4d8&dRIPjuosu*R9)cw#^k7a8g=Dp9 zMuZU5Vv`*K8qoj!-~Qki634B~>cIW>^0b3__Bfv?l*zyU-LDnJS6_MQmo7U0`fI<@ zp0dJO)GFnk&^94cPnAq;Wrf<29pwhoh(T_|tS!vK(Yo*Our5SbYR6l-x6<3Z)fFim z&;U2p-LP=#YKI`6m=S@=q=8WkmVnMuN8RUwCaN?~2sxPvA`tokJ-LlSttF#BTbv}Q z#lUp4F}h8WF8Y|L%;{!MYf4>;GeM+o87VeH4l~3I(U!s?=<<$YO3C8Urb^p9i-ott z0SJDaGx!-0H5hfr87v9X9Vzz_HwAaW1I zDS+91oS^cP(YBb|v67kd2Zf(Gz@MC~HPx)pAbc?jpMDg|R|r{g&+-f~HVX}iH_7~Q z@bk@*RX0(Kh*NZk*vuzznoPW%I$4b3A!Syvx&JE7w%uw|49xM$5E0`3=3O*PxqMq& z25ywkN+fQ8oa7;jl`_PrEPX5x1Rb=pOaP}pfN+3a=cY>pPZ0AE-TCO`Emv>##SSU1 z3W4F9Zh1Z=m&SZ@crp*q{`SJ3JS3Yo3`Ee)uqX(M)Ra<%v&lp(#!46>NfCtLM28c! zb^xM_sG`I`IdQ6T*pM!rhEQ060}QE!lLe4WOve*JH%bc4WYeHagIEZf7AxD4327sq6f0Dzd4-cU7cOUzXt;CE8E?g3Bh={j3oH{>wR z7pH*VtT;vwi!C38yse84&_Q^CFvJI56|$BO{3HdziH!*V@i_1xM$xST*;F+H+|F~| zrYq~ko_=Z(2j~WP{x=}5oEbdnM@PvSQNU?7RLfybj`_qq^dwG@+LYv&NXgO@eHowe z7(&FB1~UxkNeS5zQ3{QkuB3=$G366~rZyc~P|Mei!!^y~PKVN-srLWud2D}A$`-RP zeR*@a_L)cK`;$^jAMgP7obNq0&-cE37P~*e?2&B6vPZIQF?%F?$vxLRwfL4}4xVX$ zdDc$r+Fw>IAF}rh_3}QutgSurfX%CyJ%;TS=E)zE+G%|s`#Nx!_0-G99w4Ym7Bxy|MG zoqYyR)IRD;qv~sFA#`$paGf>U1P88`2zFgv#~Cr)p1=y zom`TTNvbe4BquZ~^>QsO-@sr9Ug`n?*n*PDhP0FqLHP+2m`mhJQOZ(WoDE^1dFUrd zgD$$XcJcS>rXlH zC`4~G+io7ulh|+UzD4?J9+Hxdql$^;QI6>ltCayx`Bt%3_l-h)=*ht|-)OGhE zi^ohFqKee?iAqTdN#P-46(EQ-00^&8FyK!lBnlHeH&+0n8&3*}w?K#-pbAC)FVCzt zjLi_lkT$mTaE2i3;tJ4pCA7mU6gbnCQro;~TiGa$@{l09QA+X??vic@C38)b(E%JY zWa`MW#Uda`Z2(74zcwD?(_KnRTY$NT6C2H$hUmsXC&0#MHy~r zMw>%kHL-|5Xu60H$^ir?fMkUsz@J9Z2MnJJHnwb~0ZzXVsNz4DpO8~B`xTT$o*ShWq)JOLAWN00%TOmEB0nR-^EO$D znWRxBSt?tKi&7#jiyQe{nIWURrIrsKnHJiar=RQ@^)*&|txDPD+7J8gj4nj}ESKA; z-$h+QeRBLIfBJm&vJLv4-_ANu_(>I@mTzX2PKZO@Y>U^&PRUYh0@=kzIR<65kWC|s z&>*_a3B;07mD=W($ON8!UKb*EL6xi^tG_MytOP zuO;aB0ReJ@)HoF3X&Y--4WerZ;5S`jrw36>)iQbz0n=mMg#b{9L>vm~L?56V4>v~n zV=Cfr9b?7-&~?+V&2B>((x!?w^*#pkCJIDqb?w4C=*ePWJyj-9Tn_%OBu=QV3AO6w zP9kTY>xR=o*Xc8C@iU+kpfki(3jy+ZIB$sEWDV&O86ltp^n5eKZImW~pO!+|%YQkre`$=%gWWZEOW)Xfjt*e@lxVqjBPKOghZWaHIKvpWx7mm;>Sh zaDrBWEQGeQRdeZc8`A2!xLHaqXwhScr#F{- z`ntBan8)+jHk$_6e)6#|`^zfjzj|u%kDgwl{obPsyuYko_9vL9K8`*A`;X7_eQZ@T z#FN|mi z*0XllVB@u3^<3ZkQk0#2<~tQ={?4yT_Ow$@P}nNjblq-%{bf9EQnp|H=;~8lwwsl2 z=-4Xmv?@Ebu-e&1v-^GpEH+>ZSV_3wtlc-?WCL`MmZ>@+70Zf2HKP)e2C}2%q!W(K zkRlyoySehy=ehbS7mFT;9`!GUcvRDn3QS7W#p+NY@QND{tw_oC81|c1U)>J{mtT76 zyG}c8yKT2w{q?V3`Gqf3Qu(-#%q?+4v;hcjsb%`QD=+iJwHmqCmV55H$FqBj*^s}#98CUF z^Tp@;ds1hge$@HryvrY7{>&%e>$PS3%U6H%KVARrzqtOYORxFn#n)f;wYj%mw`l&I z58vJ*(^)3_GocRM2t*;3}$hA=Szyqz%q2XBk`;;o@1k zD2bA>pvR_LT7^v-Sz;D{>4(t-F%Wd}Qp*S9$phU$e@}tG?=04stk(KQQeb?VtSUDeq|8Oow|m+r!)R zXR#1Crps0_Hmb`3cENN{aN^H%i5URWzOZ=g2Hj)X&MU_k88Ee9}k?8rqL)7S9KI2(@ArXAe?5S{1IcO z4N*c0!oxg)GYue^(J|*U4JZLs`0+Ff&l3p!fZD<#g{O&44`(1oA@nd&Wr|o(LSO|> z7EcNb0nrdT(^0fk2_J$8jBkyyA~T zu`_};#C#Swb1@XG7RBjo<6g1P4SSj{?|z8(W3m^Z=<%>IE6i2LDgAsR+efIV&AOrH zt%+r;`Gf!_bhX&n!XvsS&^jvWoj6nhJca-sPGSbU;+{j%+=`utjs(!*w|qO6hC-@L z0$|9*a$xv_Es*}n%H)a_UnPx*GCg=Dm{hZL7pF;Z0JBS#RNOD*e$2Oh?8kghU!QW~ ziGECwvG{}t#3SphyCW-jZOSYE{e3PW@JFy5;2}Mxk|CYaB}GL{kNbi@;3ZeF z*NopJVCcLyu_*L}jH?i%rU;J-6g8c#0yZH?N6U4^H9#sW*Oj9h5_kovL|UD4*X-MT z;}=mKLADyOq!)jgaLFxVb*Qq`vE8`8xHESry>JHd|v_?a{G=#CoMxgS);N1r|#ofHkUF)=B(pWVl#lXc!=$Y(Rj z=TYaP15T1+*L-55goXkn4kwifU3egT{Ma&02~G#3w|4<0X=t{!_>~x}6W0Y0T|{_p zpr}1P42({pf@oCGhA0G>jnW3;4}i259S`Y`IsV$r;TeD;UH}Q_286-^WSNMK6{9G+ z!!B*Qh9HQh(Gt_+d~_T_@|GYT43n9eRyR4VbRFctc?x&%yAGgtJ#?mIcNxd>Fz3x! z&sy^h8&49^nqH7;tsz})f#j9H+|(97Z>e>7V1QGO`EP&z6My@R{?rO5(_$GAg?@km zCm?GQx>zx^JP>bb64VZ!Y~=%-d-UK>Il7a`s{szVrk4Xo(E&u4wyX;WLJ6YLj_2uq zrh#e48?~0NS!$vm)MS5onZGBc5L23Yuvyin98<@s`>Oa*)N~(w-@BD~ia4@< zbakWmdpJ5C#T{IU8bujsPgW_4AEmn zQ=@M)_nC7)`>Bt8n2)~you57L17EuE9Q(`HU3JNKuKenEufBBl%~#!g-PgUyY_ECo!aMGpceBf# ziyn|pboG;9NC0F4C=ln7^QF89f=8Yp193t*oAAgk;D*W>oP9*nMk;NF7{W==5L}xq zL+J6N;{@!YGz53f0(9E6^aD7C_?K^wKJpOD8hGn1#9s5(TWql2+NYx4D zi&r}MfISq&b7$Y+X8~vT)C<}?oII57$1x{X$2fJ*E!7vyT;XOTRo217x}zBk&SJLrrq}%jj}- zc+w=Nia3XPORyNf{{vZw;^;x`<4<_l)t?3Ng(Z*RKKS6G1pxBsW407!O8)5WWPK@d zBxtj)MFdS0{bNoL9UeH7;)$S4f=InZ$R(2?aA`0Gqh-$MrpRWKX+d|CYJV<`rYkYeXjxMcoyK#5p z04J@W!bh!Po|#5uQ3}B&n#E|gW#j10u$Em!05KtyNV&)9P$1Y$EO_#%N1lGhUlV?C z{(`#}Ena9*9(q`jIDsV(*re?Gs+9-w5)hC*Yb@OEDBNc+=O%6B=oAUA#}4UG^1iS?M~Z{4MPqL z{OBeD1DMRlRKm=8^Sof*Tz}|9a?Vu?A%~ev_|*GfaszY=A@RT|sOTu5)B=&XPruS> z6u+H2d463lIVUE;7XVNi8-&#M)7q9<44x0m50N0h<%e zG0y}F2IA;sO~O=WH8I^BYp%gz4bH@`AzZtLr=wT81v!7D&+i;k;J z7bhV`Edji#Hg8T_Uku~$3szdmS5N)fYct@S#Vi~MB@m~%aD*mskphuA$A#bNj7|gt zeuzJdJl`LW{_7w7`al2jUeBpM^uXW#>gP&V-|12?dixlIA9;G^fA*cX-ZA?YJVtHB zdUV+~v7e+Ao|u)u3gPNsqwOF+^Mt8Wf+>7dzH)eFloI=4l>;EB%diej;MVF!?2F3Q%a~ zDq>2|{dy+Gl=DA93~)e%A&>|*=Cs2@CXDjgbU8ef0M^~n;wk}mWpLP(?JE#QiJ>kq z>Y{-Sg98k%1$d0oK$akiqtRLK!0X~b#IXl3u%+bD#+gNQkSD~ca-i0_u+Y}kgC3*o zlBLa2o!Xd*8|6vPe2p{jKMIG+?4T7ZXE&M?S%vHv zx?63q>Z~2t-*fwQ_T26*`|P;xp}Wtpo9yo_`&!q2+pT-R4(q>tug%|i*p84hq=UCS?TDH0di(aL95ln9U_NBmHPy>L zo$cdb`|mvCt((2+6@R)?e}8%Ef$SHo^t>1KiEL%BmpWCK)u)P3HLn`YPBFRyt&cy$ z-A8QC`OQ`lr^;4lD)ic2RU1NVk}6c&3030RB%M6=eAy+cVAb)5-~XO-Kk`9;PU+$= zU8G)CNBaxJ5LL5!8IH#T)--Ubx1lLiU{;m;?VTs;P(%f+>QbethUF^fvB7(uSqQ++ zIOnT3)t-jv5hIH2X;wB^_su)MZ6>;UgwD!g1+0&F`wP^91^##zD;`n1;ks`_fav(9 zu!U&T&m3fjw{B(cQhr6JArnFy(~NriTUUX6(EfY-W}1C%oY=Tc=ct$IK|~Q054CD! zT^bCTQWVAVUtIjTi@*FCW%4J^dEduB_TGx+%P#RHG#@$tq)}xu;-}90@I@DXLV>JO zKKt}z&-vi#pS$2gU;f;=>Se{U8rc@}E!ThJ#%sR@x$~B9KeFg9M{HSvv|&2P6Qo!Y z42YbkBuXnckQkt2?wlfCp5Wxdi70P$(qSMANgN%5f{q_P?3Q(C6B)MR$A*#~c;egu zv`G%bmv8+@^2u|(L~!B9KlU*%r@#0`FM8QaUSc7X$*cYOD>j?4wlCeS{`yzmdHeNV zXKwf02Kv+oz}2LadU@*M^GTQ-;EccnIOt+xGzJbBv2ktfJz?_?Zb#8Dtus%L9bxj(yq%}F@t9Y3OQEa`H%BS&;5 z0f?&@3kpvRx<)O5d4uBzM!D3qOE~RhnP(LD^l|`0!2Mljs{}>IR(XY9T%s;;BKGO- zyzOQTMybLqqtKcnYRh@z*9GXn91hI%R ze)_W?DV!CONAaUVZoA+3(ju)R-zqr6NxCV;^{j1I42neDad=lQxTO z$^EZ-S&ghZMuC9p)~ ztWXpff;;4#{t$u@UAi4wh+PM?E+(!vPI(6;`t&O&CHQHct`?C7hbtnyYpm`#fa!7l z;TQ_em}97oNEYyFbm2J}h0jccqi2qWE?=D^DZzuDc&?%&!c7<8kGq&#mp?vsBEqW2PvW3Q_uBZ$Z0uh>V0tmr4Aq4prLqq9w)dH?*flGRqP3;Q7VKu<# zaP2WEjpfg_me8j@kn8u`C4Dy=QKbx?cxd5ci~Rwqh2MYt!N(uE$3C6XW66E9AG-gJ zM;FgkFF*b0J?iBp_uV@0jw^iuY|;E19$zwh;hd}0%lqvzW2ddw+-Ii^_I}%Xd+)IJ zzB{dV$Zi{ZTiM@@I(WAY4%>ATaLlYNPd)T)Cm+1M!ug2ZH$G<8=11){Ov$pZ~q@Jr@_OP_N z((b2yWDjO5@l+EEG;c2Vbo6(wzCzinTy-Dup-wk$#h?;T9j9)^uT<ej9z%&L|4tZ+>m^lro)aZ3U*9 zQANoa#XDT>*$2B+5b8(DRn6EkBnme>beqwtPF*E1RRCQ}vyu}XqT02OeCkQZNeo2l z_R5MRbHpqEo1yT-B?gWt}z@1G_u7)Hb199FN!c*2E@i2-~8kRzM3S%1% zK+xmYlGVjb+)#-+Bqj9ANVB_q>zmjz83~LWkBE%OTKN6vRj+u_EB@>SYpmu;?3Ir^ zY~Mfp>)-VgneU!?G~L5-Ug92Av%*<8!W_;FLn9nccFi%wL-gD>Mf@y4u2v(<$J9{v zbR^Hx)HdU|Co2O)w1|c5Se3m~!U03FV*0g#*vDc;*D{ngsXlQ$a&iiZ7~%||iif%o zaREqzWS{xo<7%j}eW{F6Oxf#1hZs^%0f-6?eJG@ptH3-o3Dl182b-wPfVf!1hB*;A ztrXOHAzCu2so$*NVz-V?RWpM}bu1i@uwuqWt*EusnpGil!_iVuhRgs%tPsS5PRg7Z zl0usnH=Q}R-{MB*_945FNBP5)C=fS6H#;{MX_ctf!_l3>$@frp&0T&=J*?fgf97{b79Lw>ce9Py(!gi@|T#L(P3ZoU2H z8!Ecx5)ujNVp%bM%%&xPW1v=>T5Qf1Q3%kwh{aI~boo4Sb*jK`y@X?-L3N@oAZ2~c z20Avlz;y{WZxA!!+HOPB*Pbc&1wuy-Hb=EXT0!T%W_K312F$J!lt8jYKb&TVv6F&M zQVwiV=#Po;F%wk%G34OaipVI>X&4J(QE;|8StOP7h?qgjIfD*}pn&{g zw7{DpI6<>*6&#tZrrB;GByvtD$D|<)@mOfxfBuKxs%17_f1Q`T_(f-(@y$d*MIjK6t%NLvPxO2#MY`PC)KeZ`lfb%QPhvq>t&nEKy?hBe%wzD z()$TT$*fXdy4X)Zeyn(u=1OC#fMQmWuI#N?9+biJGk>3JYP*^95XJH>x888q-LsX< z_RU8XEKH+q5L3cE6c9qck{p8}MD2*hP!U0t)nJ%TA;&0TofQ2te5t;p#R={Za(FOBPNo=bg6@ME;D}bAI?~h=yt_mry?iJC(SE zA)qY}wS2$~k{WjNR$Qhre7KDm5D&OylyZ7nCVJAD|CFPrXZh9;hUh6FJG@;UVcjp= z0)&tfz6Iq;w&JI-h^UIbd@D3kId3$6YKdq8RZNT+@h~^$0gi%4i-@yHpa;V z_yN`N>?QZzKDEVsm;Gg%%ZnG@^1waU+g#TAyUg=vU-it=xrfc#dY_#(+HaSQl*~4l z_uF}W-}O>5E0+D8O2LqV)=vbJ?oPH^u;T`@ii2C%Jy_^%DT;=ctZQqi@)41 zbJZxdRMB5qs@m;i-HKDCw2D~CX-K81kjKWltOJp_!c+CiD{d=|sUogxRF??<)t(-K z^Lnxmo2XBT07Kq}7O#BctaqJi3)1Jm6yu_!mKA!8s%^P}LJwK`g=z2GC#00#K!I$O zpWINP1D_(Xm+i~U+itb#tv6jKKUes3$?pJF@%X7#jO+4~q3UHCtcY%bAbJv8vFxdA zTge~&(7QkM{xdvxtzK3mE08bvP##qxq%8xK34!W7w+ZJ$8NTf_rZN?XQ1w$F1M-DKof|+1Jl{ZMmx1iJ#{@ z5nYh;@@naB2`wI18+0BHHl_v3$^4W}8RfiW3>31-rw5)kGa#wMN!I*W6m&|=Dz(@^ z^N67nPlI!;;}Nb#hVpzh9*geJmHnHA<3f zeHGVqs!T1h3OZSIYvC3kI)%B|iA#eiG!-a@xJM78lM4E&$34XBF6PcFwfMVV|BX#6 zw;p#Kw<;WPxr$QSZ3uJEiVK0mW{i6*dcfP}AdA*c)x%ZKwD^@zEyGX&YB4ljgJ@Yf z(-JfleyLCktB4`i!c=ROt{#@|32F~f+gxs=-;kupT5{g4vwJHifqbAc5M9?Tvir57 zSiKA&708q`2UO+iWe*rITD=V6Ft-J3s=2L&1Gw1(aFZ-pziC)OXN04ghk8J4 zX({Odp3KJ7J9O$+fxrP=!KYyLy4+7hMq{q{D*1uiUsg<6H9Q1`JT&W? z5cB}fqFl@D5l4heFNNU9%v*lKor5B>%3ztRK?f2xYX^^C><$1pDf44;gehJVC442A z=cCkU9=b+%{5W6?(cs8*pYj}(A8*~@>{^;t;--;Zz$3xyJPK{EiMaB}WX zZ4j$=fAZ76QuL_@6^1|l$qyCIK*_1_R7-l%{>GcGyK~NMI91Tf*x&v0-z&5iy7*6R zxhgXu6_r{H>c=4H$%;8wMy*$5Tz(Hc^o!8Y_UFag>bUa||c&n?`br9mzc9GFKz+6%w zE;X(Ir04|L03<6ER&hAk5Mwjsh}90?40X*xfwKts?6;j|Dj>=Uf>Hipn9joxCHa5{ zz?sW5e+2H{#4AH=3B;9cXhWw>7bhQ_EV@o8HD+`o`A;od@uybHG$5{xEnJu2aGo=m zi5o>95F26HXoL6n^J;IPZ!|; z=bIKv3>EcojEL}u)Ub;gkCrXO@jC!K>qHWSEDtP>wMa>^5GlIpAs8CI!Py0LT5$;f zoBx4hp01NBL(KjEsF!UbtAc&$2zc(=Rhwd`th}}0)DQgegb>qW#+vuo0H?hBb`0g9pT;6|& zH4k{(n|ux1{;~(NJ&X-bJa|*jW}p3z-A+C7ZTs%Hk@uIMt6o+et8ncp+e`J8PDPv= zP<6KV9y{BBQ$)LEqj=xeUnHzv_U$jsYK@cMag6&o2HT)MHS8Z?fTf?>y;*TW+{crTit_AOD!0{wopo(4+lf|It2fo8EUG$H@v2B= z>3h#Q-JgemAhvz1hE*@4s4i8?@V1^gqpQeOV=7dI2GpTc0Tkt`(wG%L+I#X`;*DQ| zL4(TFhA*A!UtP1|`(}!0I-3NJD`0_N1K_rvG4ocWzLVxAYa3elTw>A0;$h2CDIa1P7YaRyK+*-L2jGVT zNSsj;rva2{XE)9GA%?KwU&bGl6@o;Uh!^x^O7@y9mgR7Szwr&Lyy0~(S1%uR_1t*;U}h{C1SQS7kWy~un#N*5gdjGb z1;wkSk1SG}-FN>zDl$c}jZUC0gQFN#WQ}^N{pGRllngzP%;hIrx`^y5NdP)m<)8U1 zQc=rJm7;#rGa)ikEtg0^ETRP1dm=t%@3$!#vdo`aN|eJ6W7iO8P|l#68T{4J!n9aK zXfuH_Tgp4;-8bb?(upcEcMvxnkH_e`6RDx>Z7G)BwmhT_r&F5kq578N5C8g4R4Ibc zArfYY7+o1$YJ?107g4l!TEu1^5nyO_4E30_>eXzU>Z(`Me%IZeioE&O+itl1j+^ee zcaECcx~OVL8CU|K1wzmo5}LNI+-cH%a;a4tD`;6Q_nf)6-FVY??w&WhG8~ab1^cA? zY;{~L+M-Z0m|=&jR=j=yF(*!nexJc3D9mOFKso>-!mTwat$fuaOI-QJPpz)4A4x2S z(IFBB#8EF7ntIs~eryCQpS=O?@cOi$p9C6&DU;qwF2cA<5v%?s_QPDXj9SkNxjGfWur)!17*v;1mrrdBHz`)T+Em2yTv@`|5YoP~yNIRR6;>jGllsCBWjMux=BE)nOx zl2h5JRYH1zS*83B|M(l#pDkJyv{KQw^6hurtXfoc{_5Ai_|&IAdHNZrochiaF{rQY zJF7PhfhtxNWp(HuE8MAVL33y-nhlpJb0{B11m*|b`2rmI#Feh;q(OPx+r&es`qwCL zImvCKqj0XIRl+NozkT%;Hky_B1S@7C!cjqM;g}&*seCiRRu7pY5&;TfoB)O0C{@Q~ zb^BGJ&1kKzaEeQeeyRF5Dw4-TOU2RxnThLUyonF{nS1xDeDEQgR*z3B>oNIYs{7rN=x8s$}W2OffsTWYIN3{i+bSuJ{ODWM@N@mJkoK%@#!>!=Gbi_s`% zQ7aXTJykAQ;u@Y)g_HFxVs`F${ZvkWrmmE+Q0kty*pW!U)cE= zgWbDtzVOz8XJ2}5({tO_GA^?+`+k{adFRus9La{4U*Gd2`FMH3#DP9up34k9x+e!L*Nr<4SUzn^za_QDJCA+V%>Mp=+5Om` zJ^j>+mXBXPXZZM`-TQTG^=Wt+5c8Kzc2liH{-c3am!9cdF*qs2IWcPl$O$i37OYO~ zY9@@f2g?sWPn$d*tV3Ykfd^S*A!RE&KF~CiideQ|h`33^!%022;+O&Ms}SSZJbVmfcZGkTXArJm)ReEIA#NZFcK;k4>9rz zCbKe2f|XcbC?9p?kwhY%Qp3XPL|SkgUWR%59M?mosa5MZ{t5z>Gn!+fq{yVIkC(Me z3X&29N82s{f!U5Zco|Z5;+nzKv{B~fJ)@gNSSi$lqR~s$ zm@O$^cRDDtVeP6Mzm&T#KruOO!5pdV;HePAcycb(RY0ly0k{8}R~!H9KmQGZQ~?(R zpezx{@<%7Jd{Cbj&YgXDgTodtoM-i+nb5T?SU#v^yIitpzD+hJW|}jQlGZdA$z3>U z$*h1TE>E$VHI<|6>V!zbIe~F0yQf#-1lw1=dP{Bduu89W z{)ufBrXUp;1=5oq6})LBpA*j0ly4zl2UA`OOb*ti-b&$)tNY7JeHq1s!Dz!6d% z3eigy+3dk-&_F_gVilIF6sOwZwIC;Lnp=26cq&JrtA-vDDhRQ8==mB#&5QM<01)1; zg>W=nXuR@pQc-=;py#w^lBko=q>7^Iq@sjtU(7xErPU3b(oDHvtMolhLr8;2Q<_Eq z=j;qu!i z1u9U*3gJ>+Dwao;lE)LhyeunlAH1eR%2bEvkqbX!%)LT(`O+aHQC>LU@3sCQ^{S0< zHa2BVDje`hYhr~!EHMO1NC4TTu*!lg#QX#hRq53qoAuMO8iGDaGxVw>;Pli1f@ z_vIrywr$tGRg0EQZ@lTNfBN(9e)a30gVkUr>62hMR{ss=_g>7;61d}n316N57eJS;WMhHKTSd)qBGDUbwBFeze*Ea$YBrS$P85V+MnpfHBe*CH4bW~4) zda|eRfNqpRP?qFaaUm0)elBhDk~U>2{Oc1WP!S9w{;X)!0{%Jc>g+N|$h=nAr zBnUi1leG2gS**My)DT60gFKQS zW;V{u7MCgA++IQs@;Hy}tK?kj1uQ#{&E+IEj9iXt^W-)#F!}ojUVQu2mq6x%m-F%R zTL*T%|Ms4@U)u#QKf84eyzDDxc-f8FySA&l%gZZd?zaFmsM%FPzYI!T2sq zXY{{#+1Sg@opR>flgFLZt#8+spN5yA$Xp!^?SjJ&H$unDmn`%j78WMQR;ia3iv@FM zdbMQOcNN9ML4VF6J5=oCwpakuF=jxOAAELgHIpd3?4~avPExTiGa@^N&A80(o8{Tl zr;6fcPMa)BAlQf@11=m2R645`crJuC0^7=hoKq%^b!wVYYS-7QGsE6uhA`| zQz7swN_Z4AnoL|0@+VVIeyrl*mnvBci6(}Fp>0G<8)D%l`9U58C?%mT(qweG=tZ46 z904i&ez|A2j?l5g*fz>GZFtBT5+_W+B zuXB=zcu9^1B`0}WQ2lVJL(D~0d3?1{cFmfPUOw2G^PV@2z1gXf7w4VHO_f!gO zKheouVQpL4s7c?5{8W0cZrw328Ht8o3_4h0NF%i!2S=ENeC!~K|h9FQ)XG_Sf9aOm0v-=)tjs+50*%aW*k2S$?8cP zq{vaXtkh^|hImSwmgUD5x2Lx}28jdC{2(&|UomrHiNukU<56TytlrW~^)%g#gBcy# zvH7VJB2ab?aXPRf>!0Z=VZtw#A4(-;pewc3D`}}5@v|!O&;baqT9r-_5O`Ih6v``y zgD2=6dP&qwvi7y)*k#PZ0IFk+*8rs32!3QdDF;6-ypy+lR@MP_44tfOegV6X077Szph zDtq+E;S#1x7?0s5q-WR8$QI3?O&CL4?mV1)GXM==I}%n{r`&UVPQ@ZqeC2gegr&fg zVk?!x1M@=GDu)^~YUEJi{+e0s!ONH{vT6x)^(G5`B^7H5(kuLdCSg`wd?Bz2_5!4C z#&!T3Qa-GK|0y|a!NNM`SRo-pV{*wRGbQ1%s}+ybJ6l2}Cah z!YvMTl^vWeM?yliu&vLl-Q(>%wkLVS1}{sChF}C{3zili8HvS2l(e!C2&xN54-r51 z_{Q8GhIxQChWv1}E5#iDCRiyl`iy-Ontr0U7^KE{hPGFIAVb(#<~OeO4V37G3q*rB zTWv-;-6g5@Jp+I)=ztoKg1G-XQ_>Z zq&S5wVtyGVFRbKFNMsQ*$-`6A)MtVwd@42Y|B08`mjPjr8AkSmi*vOXvvLBR$>b6% z5MBnBfAIRtZvD#r*kJO3J=>kie*3lOfAGfhZ|>je7&frH*WXLMyoGW3t^L~=m*3dC zW&iV=KYVY`vbkfJnB9-$~?ejUkiQU zTs@CHb5s{@_Ly!CX1nie?wDg1)b#w!qLC}kn83a~X~c0u`gQs=ybS9x*B;)mVaw*t zOz)0Zn_7Ko3xwLsbxsT-wq*@k`fFPMS_|UwGu$AConcRg*LicMw`<)R0P#_?6{khB zW-XdEp)p#%Z@BL2(-+J|d}P~J*I)Y;_GO2z-Nr=`U3U7yZk>;EO|e*xmDxp?fuB@a znL?Ddi+vX?*2XVHX?4pol-p69yQXPSr$KdKnFuXj(m@Pq1jGF79;C*T@AR{We|;tbdlkNo`d3V}cB5(||x zgt57^YEgE$?0%KnWmfe!gxcuU?k5?{ruWEEwVe2f% z6K%>8oOM}9(T=RYXfnjIG+U!h&yt|Y!mT?2M06?~vUYolqEjKJV#S5Os5sn&)Q^=f zCNvY_(jve_*8D#A59pj4Z6OZrdB=%2!^p^jV z%1vJ|G7rZCL^C%7(fBAGM>+9~NMxg~aB7bd#fc&eEGDG5(B!Eg2-Fo1R-P#!JzR}} ze)$MdII&eACp^s2BNl}f4vO!!h{Uf3(10o&Ef*`tA$;UZnWrQt5L*n%h8_p?8F@VU zIh@rktOS=iqZ^pa3dsRhIT*~K{O)^i1xW#P@Dx@OY8rgBNQA*H9wA}QLr8)2Po)5_ zNTozl=@puly;$8E_~xaK?b{fMpE>VLY-!y}D%P+P>!S&$M3`~q%i(4c@PBQL&QJzu zY<>VA=(p)74!}s&#$Sh3fwoD8p^YSJ!&9|ec4;G=gKc*3`uFaMe)`m@r;ZrPpqwa+ z(U-pXxvs}_Vt1B{%N*#{N%z6=)w(U9zpXM9VzG3T+1^`8b9UC~-ue>JBH(g`_wqh(qYXEoQ9 zoGdh}T-@j-7ynwtY>ht?GwcjF>jq*g*tybw3F`F-ZlchDodb84X?7MIR=rLUH9VJTl?Rs5sPJB~q4X2>f9pM0x4P zi9$xt!Gs4vV6`W|kc4~68s}ncN_%C#^3-pI5LoF9E2CAPQGBJ-!OSh9glKm)fKtKJ zrnL#T6At-ESOdYUPKs;~ajZ?!m&rbOvU zVQAl`O^c?D=gyiwbkIO*!!=*J0-AOCIUAW4bceP>Yszk7cMXg6RCO75Do{fx2x%FM z$?v$DeSX_qR112GC6!=$sVMbhL%V6?!|ZH}RXST-#Ts$DfjE&uSLrimt1-gD9!3R zYj$n3=8apnYSyN0%hsU!!ny*e*`Q(7U;Oez02AH>A#>mvgp9Q}y~|T2rztf7!Bn`0 z6GAFID!o)g2dF}|@GMp=#Vp{EFJu}+b3kr^%qW3kjt7!O16;$c4r5Eg3M?EkA;p5g zJRmu|EHL;N0a0-vyh(q0^^y$L!YJcaX_4MX+%yX z=osqU6La%YRt>MEdtjI*OSO1axPeO2rOTNjP zzvM1S2sE^C&fbNxLY-T1g6PA#JJOw60$1e5aK<3wC9x?)&lyGqWDgWyWP!WS={C%!P|4o;!CGusnNoA5R}I`-ItjU(03; zSum-uBia5s3Rtcfmuvc5FmI46zh;f;?1c8R*+U@i$s>FJH}_*hVb)Cx4QE?5-{b;l zv{NC#Y0xDzE_}h1L+fT6b;)y*IH3e@!q>ncMUnQ%{_AL&SO^hkA1oiyq*1fRmA`&= zxup}=)-``J_2lM_ja@+9oPt@&x^t(>B^$G9r+|~NAs8-SOB+=o1lU560-2m>7yju9 z8I!J62@5p7n0By@ebyRhMFSAXsdNm@ti}Lucq$sHXb}OS>~1^~S}0kfnX~1>aBNQi zPn*mzY?AW;&WdjXAB0yka8!M z{7My`NsE$fhBh?@rB@lr9TP4^0Hy@eX=R~t?XggtS~NS%pGo=uWq-u%&!j&5$vaT9 z|6hLU@wIHtJYEBq8JA5C@{NQMhNen4rm_-7EEp8l@Owq8SwJM#A=y=i3B_xb+u_{Uw4`HGyNgI8MNA(g+lv0SL zNOJ;9%L>O$S$Oay9xw$F15>_c1(yJ1==i5Ud!Jdka{9XZkIcd$IFL$+4|ws^7K|+N zk_-MRW|~G`nop5s!4W3%$W(Q4A*R*SR1Iwti_aqb9jYdyeq)ORdztegLQDlTThjvA zt8cJM(^O+YrsCSNLjyijl^v?2@!Hc9lA{GY1~}DMb)^O>HUG>uij&2QLLPIKzjnRg zoHM_C#phkSZ}kD<{`~vj%$QokxXjc=Lb)h1Wt(vs3?~v9T)mFJxdl|$rqGHaD|nii zsH!E+p*v|ZPIU=a8|l*N82k1+16jeUkTRDFw|4=LNsbLUimqmqNN~=)s;~x$gk^i0ud;Sa9`zESqyVSScs_fYK`kvou~>?obG3-wVXK{Y{M2tmONu$ z1*#BhlTU}#kDBKUaBR&0Mv6?$X;`UJS!)h}&cO+v-&z)}*AvH`suMbEfq6bJG8QI{ zAEp0WjF4G070@I`S{$^FC(aoTJlc-5IWVB1(>XPPMf0<^eji7FpPwlCpTRj08u2VIUzPlcoT+cUZS>o*?Np{a zdPB`r1?rz(VLa0G>Sop|X@@o~t5;HVWIiUd=}}|Jov2FW@0IBWmTwC*VzY$#fA9(Y zw1`F7KHJ~^&;QaEs?I1@QBH-q5mcj?TU_MA(2pKxgPJ8eEuxAviq%|`sYzRN_^V;M zkV$~nw07OYfBCmRfSHbD!+?GJ9oM#9%k~{wJ8|thWftd~ZvGl{e8!neuKVg&8a6uY zn9d!KKIX`N{d)}>e0+x^+l)MAnE#&wFkxG^X}A(xg*w4c+$yN{b*BPsTLH_?KRAg^;A!qlI7<<1!A6%Lrg$A^1qkAt1IQ2q(N(B|;1i`xII(!j%Z6RDiSU z!k<-LD#c8Wl4D+GHhsnzhnMY{JpSl<-5{9P0U^4jXAr8c{kdZk=Dj{fu#Lr_RPAn%1 z&w^QS69Um#Ujkl|5kKP0ON7LS%wCh1SNUd&LoaRkkctzM4;gsyAvTYMoY&+Mo`g&R zCFewW&5T-zg^)xb0(`<#AcB-AA)yZqKM9=-@gzw~jX9Z#Cy>LigySF~o@jXDyb=#3 z`MGc|xr@b(Q@5V=8RCpgop3yaK`eQs^~`Ikg`YzvJhjc^Rbc9Y3`xktg?Z{wcmhKp z)hD`7liysY-3cZyLPk!Uxk%-i3eoFOdgb8+$|wa)@=v1=he(AolHiaA^2Edji*N<8 zpqRkT)jTFotD9M!CyRpBCu+83!W!h2O8nCgP`Wq6ZfeD*>{K?q92^~6^N-%6oTFz7 z+4ttY=VM>4nwWRHA{$tai5Xsg?=9DVRqV@g{!fZonTh$uo$DRU-o5n^S7iG?scB>S z_)iFx|iKE7XGJbmbr8N)!`nPYoRAKA;5UvtOxuRXcPoH2dRoOSZqb58b`QcG$F z<^QC7zwA8rf=RtkpW1)v^nvg)6SMzchMgBq9pF6nyvZjGJg(ib9h$XjUa@^Na2DvW zhWYbl|H2%6H)`ZaHW#tBok31!7kkv!6q7%jDfDL@vHqSiVu&4Sz|G$-v}jhbM+>Jp zz>L-rudF#%N!ZqIu)i3lKAf8~aa&7io{4wS7*QGpkO@F6P^>xUI2j8dC>R_O{$T@2U9Sp{Z?49u(`#Tcv6uN zY9_M~!hpswxZ1!6TTmK-EauFd29O2v_;Oi@C-_`3H{X7p z&z6DZe7x)oHq`7ew(pwVlKtqK@8!#7|0l(`?2CJf5WOg>k&$AKkA_TBGC&L#fkNm8 zv^CGDg3%#`h&9Bd^5hT*5zd7Ye+mo^74Ug-1MDCoiL0pm~4>s4Xastd>2xcI?}$bFUspu`+MoxXREDK!%;SJh|SN%WnFD zmj!~)8`iGGD)mEhd~_b|DGW^DR4(2;2- z9?Co!D4c1Fw4+?`PhnFb1oNsmX*IFrWLZ1*=#Jng#a>HkGQ%O39sli;9W4_M6Ejto zimp!VK7qhk`LAvCHUJ%8vAL&27z&}mKD2UWj>njh5UV{k0E3W<7?u_K&(hNtOc--2 ztJWDym)hmAJKRcK8uZ8=HAdlA3_6N?`TSf=lGuAA9uC z!ilO}+EHs#;PsO@O^~Kh1952=v5FtZ4;MRq4ZW)Q0B81PPAjl1Z`!;Ls$~Tht0A-k zs903NqD4eGp`}o_f=1C$41@r}u~z#U8al=k!`$@c)saGw`3dr$z6rHtnd?8cV^AXd{$g z$4;m9OcxTK?E>RVJ)ki#buCW9;u0c>O;kHVuB|h@s}`J9k(x2tI*?%B)run=e|&Uu zJz0IKG_{S1#ZJYdw3c;f+ZrDoe&p~Wx#!t3ZH!pE4I&<63V^lyI!*gOq*tMFjQ?Ls9WvTWz-P zamH3Pg3^+J%o>O%`yQ$>|G7DS%t-Zc**{+FYY`3Md*myw{M_$-2mQgjEU?pRYuJ%H zc53G%<+IOOe%a+0&z(2xmRoNDoZ&||c*4$-3>`LT-u&6i&sfX`%{&cGLXjXOW2-0h zniI|#G9J1u9!BP%oW!t#Q1(ZH}!wzPC9E zXF@Qj%N&T0aAZ23BnZ5QUJ{=8Ar9O!iwf84yh?%|f2bS>G@fY0$^uy^z4DlU&$iz?l&6Sc&99R5PJs%0(F!jpLU!p0dN12eIVR;+Cvs!IMc$p@fS~6pvVf zNiHO*a7d=2LJ&kIJZVz{{BflSh`B;0Hhcu)?~InoTr9sN5i(h0D0hC8Qla6{gnt-D z7&NJzaIP;vDgi+_c;c2~@&lM$*kZ*aHZJtZMF?Sppdn{O^(rvpArKW|_=`MAk|@u@ z6WfCv$s>HiQN}5k5Io5&yOLvK!#QkG2ITT^i7EvYf(#48TN)J6(YpJ6hU1QU>1 z@=B_zl5fuBOn{tOj?VmPco`6OCez+04cYNaFq!+{hi|~nfOAl@CqOO4{XYTA?91>n z`*P(-_N%)g6zGfzL_6K4Oy{DU`kF)Q!hzUJ924{v|sft^o2xNY-& z!19CcNlI<)Ly)-srC=axRk*yatU2VpKZ|t^KLrc8fY?|tZwW|a=l!!T18M=gI z8JZn^q&C3FJ^*B2c8tn4Aj`7Pmi?ua9plLp#`-fUH)EeRzmBoSvPeO-oXMTaVK3Lh z=d8Fh-hezj4pah?0p?bGS+hL>l2#~^Sokb+)O8>fdPQt~#0vfeNI_sBFmC{uA zgg}*!{0z8c&NX6Exvqx}OvkXj46CY>)RHG4K!KMXWy6*NRGZq9oUTqhAXhFpgT3H9 zRR=UClBHJtK=f8vc*p%|_WV_KP&^6wp=>5;$H{0=s+7#6bpTBLD<sEc2Wf@p@-xufO z<(Hm&0$!#CMUkT^(I28B3goC9MvV{}Ak8Y;zK}Svv^Wzy0>sg~qD)4UjFu(VD@v(6 z5nx5xkVF(33Yc&nQ9{B&V4QHtIjl?WS3*R^6Qyxn{(5btusfmS)5m|x`(n#L84sYsRn3eR6$up zixw+Nh*t!%qjAMxq*l?(It2Wwmeg&^B``GD%Ec2y+&syjta-Bgr-v}D>QtQGr9ZQx zvg6<&M;S?oSPiHadcF|4pWJx}Bi1ORnX`%5*P_@9*C>=p6@`hkBy=ebo^COsWc@e7^%pa3Nh-H z%NW#5Jd8mGo!u~a837@qoq_;j@I00rJxA$U%iIPO3H{m*~EA6}-v5s#WgMLwS zJYnQ$hU`R-ZCvL@3xA-@`KX*?9J3Mmu~R^rh7u_YSJ0O5xah!%s;|l>xmrRX95GLE z_;BHgc_t9UQwLTwr8bC^MX8lnlVy(bj7OB(Fq#x!XUj2Z0}3%GR&xDZ&&Nt~la+H6 z7CQ$LY^$mbWI7UOMwPD}V@ImgTjm7YG6oTb$q_@j=-c?{`RRV1;4q$sfPTpDX~opx z^-POqx+uFdF*KaAki?p(fzm(2y!21)QsxkTc~PR!q(mB*U!gc=W#H7{wRQuH4uKeS z7DQ#SX{JsnY3f#Ot6MuEumF!8l2p5=b_tvjj_fag`0d8EkNAv>ec9I7$wLRvn>~X% z;R+JRN{vmRiN5EWl=IK7{N<$~PZMATFiF}rR4&y8%Uj}ogX}z*12Fa$9N#hns2|m0 zM*^`x#P%F8QC${#WlXmuyoFx4y$BGS0h*d&rqvdjNrNh~6&!(5Q()8~1%Mc8n~nIy zK?4|WxWMUnQSPlHg&B~mMw>%D5X9aWa4pgrOP*L^IEwq zXYSviwN^~D)sxaevN>PPbPEW}0*D53IUykc%;91VL2?9mWBDZ&8VtFZo^c?XKv@X% ziL@d^!65m=k32wl$TuikI5K4+KxBZCgR+B{1DQPuKo}fkiWRM17cy*h5B)^XFlUL_G5K7a#cA*0|99>lnXVP;g_7*9H_ zX^H?LKnc_Y^I&!!kPKU7DMg`|yyPX2^GX0t z^YMqEcmyVw5a*c?lwSD@2Oh3IWqtoad{U5PCwCPkFOH`OAO0nw8c-auNgJ_KKr{!* zgDmaE6Ncy`VHhTmKWsf^hrnzZO`a!6IlwuK4!^+ASOvU-#%xR`AJ!#$GHA@;;OI55 z%$aaF9+Q%($&3UkzxU=|?w!~60Lxs}th~pcN%@4?<=6-IY-I{Ku;*!x2UvDF_A^gC z1TQl#!^=}f^>rTGeP45@3|?3}1YUL+d&cPAzF$7Ac94&k=IPTi>-u zK6CakkomMJ1N;SZZu(+kW>a=QHoUxKdOvq%&#UR{ME9JDgGQWiY{ym&n>GFzFIQB& z!z->)vmiCfCykUf$NnnB$5}5Bx!@welU>HEuYMpn+K%PJY72BYbQH5M3oHV7P+ES# z+4s2=0C?G-Nr|%3nOOV1#C&?jb9J*i-7Uh9)j?3qfhSDqhexqywkdzy#?QW%bGr~ zW~{m7XQ8%s6U=f9+K|>q;`9e1S*#&7TM94^{_+K=@h267z9ed!B;*92ZD{*Mn&iaD z=V|_P`QZoeVqEqKbMC>0mpMnZ%eC12K67Sb_Mgjc`r?9@L1q?b$FLu|_YQx)oSU&d z{RQ)L+cv)PeC1G?3ZX{@;829<3=}7Wi0YVEAyk6sTEgQfR0yPsmPLs=$kF{cW6eOL z?{WM>&{I?p^J9|Hij@%Dyz|woWxN(1(XqbnBNMLoaf_(3PLR%n?vZi_5-f zTvU;mw9HFPc&Z&+d@eZm+>Y(r8~DHi{+yfhwFre=6OfG&0jd?-arNVI9$=XtSaw)C|Ms=7`o~`US$1g=JhUmA5>XYwlR`+vq7!<0&FY*( z0?rBG0D5ca?1yOLPqZ&IAvGxlwFc2u0=7X zpQtOf6#BwPjd4;%{*>A?O{R?6%Yf3Fr71;miX%$t>=7%G^&DzR(hkR)Ye|%j%faxgGZ}*(%&5Cpu&0jRM|HXWTqsr zFY|c))z5zhE1At9lt2FVUo>v>CQa;L*?BP16-@=|V^FQxz%GHg!aDvx{^QR|B;1xb zYrCm~!Ci?==7zA%oPquOTNfrz97Ett(e~)nkK-b05@bHQV>^ZeYrR2DV^mAZl%-j? z{dNhtSny98dScUNjc&XBX2+^QPJbo^5d+aqWivFNd)}Ggp^tNY4h(VvhE1C_?b7w= zBU&^8nH|-Juf_Tt7`g-=oyP`O;nu&~bKkI2;lR90=Qg>Wl8FacE~r`5_AQRvt%8~@ zUUq|HKE?s|1Xo##IZza~C7y%X*3H>|$z{<|H$<_C%cs1YlRTk|R#OJd$t- z__>IkKum-SNiOxX4#A;MIm!rS{Ti#}QZ*%;+v(L&)T6LjmCTTb;${1C05WaarkqP( za`zW4nnyTiLl0z*aoN-aDTB;au>AaMFYk0kwrjrLduz|G?T`ACOn8}NS@us-!1Au` z>-_VwkC)?K+_Cn#r&qb`YtqPGlTYn6WBh>J_vQQLnPd96AA7-+ffp8rQROqI=AM zQwDcy*ZeRhT?>t0gN8mGuHfYcjhkB~;ZW+g>D^jD@q;cQI8f9Q!_W!yT1KHiiv>7L z)e%BFbtK!`L|udp!992r^O?0Z03a2V4q)l2V8d&^3@3exQ{Kt+1dp0-UMhpfSu#W;+JZEBI{BS-27j2W&A1W7Pn9#$pSi z1LsgPP{7iy?6_HWRhP3~)=));L4b6`v~i;Pa#PP5#KL!j$|`2(-5MP@dTMt6{rMM4k45) zu|^OLf$~FrGO~(`K1tmYmB8p-^`8<-UY`;%p2nl7fM2m0uu)_92_Sa_#%#u~kcbiH z{K$yv8CEnFG>Mf(q9}NjI%Q#ep`+64z+~sKq2}gI9Im+8F!!I!4r9|m=otQ9>cRWJ z%>yqR`$j#*PiL@6LJ1@i{&>=mQ)&hINbRqhlW3CGaFr%d6iPZLKiSA5IY%}wet}%n zIdvOi960$3Cpny``|0pH7=2imPzs%j2R77o5la}UBvA>nle=(2;xCFwk;+p>v99tc z2uW0+B%;UDMA%^>DF>&A=`k?C?HRRDl(xxVMTXT%g z&Pl?jMxQduNC&W?p+p)wn|`S!133^LB+-k4qk$9(vti35P5UJw{l~Y-D$_L!6msel zUFDSFC%KK#)8`~vL#<6T0T&RN2VfBLlS(_15KqYsWFXyS0+rCW6;`z?mnS*BDk;Y! zm4YjyCZ@)#cCToJYb=`*Djbr{!E6WP;bkEAr9HbG!)6GEk;Nul8)e-j9*;FK=p)dN zauw&5E|=9AQf4&{7}vPi$}T6s;bog7@|BC`c8WkQs+UW@RcZn&GMQ5@B~lL>J=Rr# zF=f%XEUl(%+dcxtn&(krUd zNIDgULagI@ojJ|rRK8s@4_FN?>)@)|W&qV{C<&GgXT+8~^9fdRB$9SCr=>-dDC1OF zh$;%1BB#8HgIj6?gjZ^1v?*j(%pq`K*=RLE*?j)v?|$t(b2_&3*fh6N!~z6pdLVo-z9 z)^3Zkdi(uveyM1Vmyk-4J>g~J9rV^BW*)O5_k}NB_KRPB2$n*=UYVF%wQhb~uWnGM zubbghfYiT7-F5f3#P;afg^9WUfZpz)77j2XfEGKD%T#$wliy|885mi1eo zUibX2ClBm<_Knw`f9=37?)z`PS-pms;Z{ZGEyF&nBlVgyW+iDO! zRcHzb($xtZK4{{Qk%xp{QSxJ}fEJ+Gl=+pN-=4sx^=n9r09hcF?PfQYJ z(D9HQ=Y$(C0*Mh1%7_$tG#QMZqEI3ONa0pd9I_IXSkGkV8J=i}M6o7+@(PEf4WGg- zf#Jz7JoPLDH~9)IGy;ngE632MbfO{*&!MTX6&?xsNyx>634dmOp1c-e(ngW01R_)C zYTJ|ZS_C`DgBw@mNv)<@gkvr(7x5D7S&ASbzYw{^k65ZuN>$3PXl3gl#gxmT4j-u^ z4*}xG&D<3o+tf$<0+1<1ruryvhvH>em`&MEouio4W47iPlmnT8WwGqb!ON`7kTU18 zFaN^4|HbVGUfRK|{DU`M^e@cZ-WQ&79((tW^}b(z?Uk)e%zK{O_`);me80@S!oPjZ zoSMN?NB65a<+!P%`^=s=VE)uWr%fBe0n77h2AnZ-&Ru(9V6BWjQ;sjDi5YSAD z#^_ZzN@?SawP;o$vu9-_hBGn_y;K4Ng+hjaqLM+%lU#U8a+({ql7wEAB(yt$QF0DG4Zn390Deq*7$(Oq}*q5K?gtTcSU%dsp@{1BcQp5V1zVL`8wy zwzgWgXd%|@VUEICofFBlZ$P=yUp3-vhLIzI@wBby7B41yrc7%JZO4hw31cevCix>~ zsyp32YqbBC@-?OXE87?Pp)#6|v=MZ};%s-(bPBwfGsP++$rayIo02>=ppxY)1WpNF zR{sj3T@X}iX5WiYYK+Kgp2w>|*p_iwqhtre(QcSNL4`Nz$ zIoC~nrQfD1XuSNA#1%9r6@rx4Q(I`1G?q3}mb8?@5}qS`D#755aT!4-C`DFLUKLiI zaH$aCFdF1#1+k!5q2Mwai|lk`a7=cV4_i7I%3Y1gNMA*uFL~l`k~A0D_?#TCxj%VMS!zGp;4@=vX-xQ#5@g6Y);lPO}+{% z3V|(O|7a{BkZwjYOjHt9p)X|wez&Q?jBLf9@L=uASN0$&0Lgi2R9qoWK*{U&S3Zbh0`g{q|q)&Vvs7V z;R!EOClpO_JT)ScVq5`vc0;kX`APm*Qs)+(!g#)T+WG#@i@&#`pU$Vi}q!LKTKxRpR zWzu>kIXH}r^?dc3hZwTu63W0~v339eVVUwvf`GYMZOD`d|Jau?As&IXxUsD1btIic z)9XA@`wjdIb9NbXh*&7VGZTnOL&wXcC(yK@zX2VdnIGU~I21#aW)G=!MVvFYm}hW9 zoKzBKYLaQdvd(O3k{p4w>Et-*w?YV}qn61E&-7|M<8X^G1mlV1N@3PA6UxFfos>elQ^^vDueh_T|r{KxT&K zJv-OC@@w{_LGZH6zA8tuC!gR*_R^Wdm(3cn*l}x@dQBd3`jnIW`7-13;;BRCjUV98 zq+s zsCpQiiMc@&U=5msG2uj@4^)H&AObd1N4c1Zy*de;&za#kC>W08U{a9Q*(dw105#xg zDPfOB99RmjTAF?D?^;XtWq-)xB=#c@-VYBC9@t-qQ`t-g5Hw6JElt(s9$48Dr$0ci zILOFpG>aEYt5>EyYBQajm3hP3RTN$5H3zmCqb+%WExhlDn$-$S_9Q;OX-yi>%rs zp3~Y?Ws9<%YL(^`x9S3}0cvR4&NGdIR3Ni5G9?4-HUWHt#y+JQm5YXNQv#;5(8K98 ze63&!R#(%fj&%Urk!&9?`!gv9Wk;{!WvJO5U+^-doQuD1z2QoieO++Yg3tfsIk7DJ zhbiZ=U5@P&W-!??Y)|fmosan@k_JV!g68MM>}! z$Wf)Z1bRh)ENLa@FysoojKWZ=M1=$OEZK!;(s~jm>Y{K=3}>k@5g6wDijaJxqhcsk zb-VYK>N(ycs-O6^I$=PM}c){hP+cHAhR#RG6?|7&SN|01~LQDYDOp2WK=IW zjuuZ}B10-lMqYyp@NMFR#c_UHqzr10E$OB#V!#HogoTok6vzL$MK(S=j)|29yCS5V8O|WU9t|M`>fw2;~ zx!2gVwP+JYS>)&xsQn80Qn{rvqZ;*0S2(BUwTQwJBSS+;HG$@kyX0sTIhF8HjaBa< zhhzg|Res#Q{gtc009zg*Vr^nT&C%1MB^Cf^v_uW?$iLZ$`9;*i=%4dA9(X{yvt8ZQmA zl!BbJ*%*NAsrs%5;H7^mi{_w0So*E_Q2>-nLXL+rz^R8EYh;>XpQ38gLbV_cvP%=E zt~`?#=TsV*OP}OP|IuE`TY9XP5>9@=5ZF}Y;ANv&62!`-$eK9i6`~SS1A2~?i&<7j z(!(7d)_bK=W8fG{{nIpMMblvf;>pPija)LW^Pv#b3=ShpyO?A+1Vj=-Do|19%yA1# z5^Haz!mCr3NeM$U8<$By+eHkSDxJbAra@isvRHlIIMTfxXgoEqAFCN z%{sB#!Y-v^TH;Mj_-iJtq~d^R3{BMPghJ&S2Qbjb%l=6U#6;{a+|g-%$(<) zf7ajr_Ft^JFev~FErOy>V&^t6sM#@Q$D{#9xVV^uAx)To=kS}AgkOYFLJR|{Z z@%Pa&N|tEZF=ueVZ z2*79PI0wZOj!ar-#Vg>w`oX)|mTqsnn;CQ%uZFqF&7=|7YXZ^7M5SvP{z0tiUAk5{ z1g!`(Z9=1_1wBea^g1N|6$J4y4AjRYlo5K4N6(Zgk@RG#^i@wZGQ~lxTr3mm$&wV9 zgH>F*x~G{(c0A!M2?5AVB&@_{rXn6ug~(KP@+F2;=sElWC44ZEHK%?utb}`&DPm+P zR0`--l<-8MR4oZ9LZS&GONDp}i9^XrRVoT)vLl9nkJT0kunH4VjNnOCp2@w)hE*6! zf<}0egkOm8NP>)Cti05KCqJ4L!7JxA0`sUz1YxfDYA=-_F!BrGWZ^ZGg@2fPmE7bb zi?9_E7Y?OR^*tddAtZ?dZ}Ac9RdyU2uaS_ygoi%TMmEIKrld&{$Q*`ZRY;MrJ}XR6 zdg^1klTLy;%8L9|q--+e3UD?tnz|Dukl(CmA%LkFj$vdrW$H1Tvd@`gajuV-D=_(W zXOJqE=r{K6hL_#?<>@f?t9zb-mzkK^>|8kf`raLHzvheP=N!pqW!|**uGe0E>XqG_ z7?*v>>^!!=l!BLYKlZ$-Ll#UM?EB?8lfe*EH9jN zyvwnxZouhNk9W=2lG@{4j?FEv?ZdtdFP~j^Qr*PfQ$}`WUmi2;xPq76kNtPNeAwX@ z7dVt56@sv!z>*v+%pOYFvsD}5Ne#A0L2cGkC=U$~YCjoXMhrqie+U3QfENg5^MzaC zQ{Mx!Yr7xYe?U-j7?=GEvx_bnan4wBnpa1%-3cHq`!ejjVeKj=VCWUtgjKPk58J=B zhy#_ND+q6C20yL%j*f%F^bNbaG-qN+qC|E85L?{{i33SeOtB!gkf2cj!D;OvWyL2$ zyVe!$TERhD$(h}i4x~jdobz!Oo{Z^I5e#lpKcKx2lUV_f(Rpk>FgaOp-GRoorRpdQ z*-ucP{HlclZ_V|Jp4f_vn}kFq36w2RO;S9PuMffCAaJ{B`39-S@Tf?prsmzGvGLt086geffTweHmV+ z#nJ0Hss%kE`XSYbT1EBpq>oXYctja5p2!5!hzz3QC>uhAhd$a@_=Iy}h0qD3I`ZJ0 zf$1rsSRM|yuu3jQF8vM9;)He@6yEv z4y{|a)WPH^m5hi)49W4N@(CdczYyVQh{1VPBDysFnV;A=Ad@^u&1bC~I0m4BPCxtb zN1wUyoL;@V(fZo8Z|5*}_a2@9;iB_lF2IlgI1r3@S}IUcUUF2%~>Q4v*Ca;avGbOk_M~z#yFi;aVV?$4EwUdTa9f=VK#N(pgFW4 zF^H$_{82Jf5px(sPikPny83Wg*Z_XDm89c1us*o)rtC%9A`TrsSr=x zxKvc7(yF^v8bx#HRAvudniC$eMQL1q31t#J-8SP^t-m5hU@`qX*Av?M1LT!w>r3Eak9T4dxQK>DOQPZGPk zI^@)v8d(^ahCKNVJ$VVS=m^m?#M2@XGXBgP_$aI-@!)A*@^=`PGeFf>|Mq)j>f%91 zHH;~3Wg|{W`PBe=&0+gM<*Rm2^{mxYxZ$SlRI4UP3u<480KHgOd!cvNJw%*;d1JRS z9yPHxlJGab`e&DByIIt>HmELlVyIi=owSa8SXcph+gYlM8eorIqbN1m6kMed&w&6zaIHdWLpS}-f zLW2D8@9_is4m_dXh?7US1p5zv_zmN%FO*?SI26WgA zEI!BtLY=I=Z@&J*3%j3s`pGq0o?83j^H0CJ|2be;2-M8J{Qi3fc$~xrsGZj4hd>D< zQ+&Wra2_8Fy|N3-hyz!hpl0C}8~7ZsOi1Qn$oHP_edm$Y5Bei29DrG3NQDwTKP(Is z!}vlT-Q@n(2b|vqnPFb|T6SVX24c$vlN5>=I4Cs-l0)(81Wj;v?m9DyIU`rk$(ZHw zs-Ji@z`;n)D5&0E1tUNi{L&((o%$$Pcf!Ogwx$%*evpwT zVj(!_u^hjIl1qY^RAv?V+U~O>hXYEeM=mI(4T=%S3E?miivx#)SM;9xKe4ej1emLh{xfnl@(T?D8=zq3a=qZNO-2MGyzt+xZiq~PTP zd$vO#&SSs$?B+eYHam~)itHUvJ^c2oJ6_%Mq+7C?m|tRL-nM$~l#_hI?Ej<|)DC4_ z_LoxQ1|L0k&{4C-^?IokA2?!;qJ$-9o1vnsD9%{^sbMW%lFGZVTSlP3ocFj(u*&&=CYYuPy&#x z8ql;g!f7k(2>50_^~&@K*g8=RbWjex1`i_hof`cXVr5(w1 z-!DJ)=&Bu0u6u6l=9hPE^-tm)lf+myP6-f(@(s5lfV_~QF{p9FoI^|}q|IjdQ)yDG zY_e%cS;Ey3W~o)mZM@xD;6ta z5J|1#i5odRX<29xc$Eq#tBgpB@C*7DTF6-b{;rOTu1?PAq7fS&7ruecGC?g^ffIpRHie|vFePn9# zO(HVNyiu$<6em+U)zpNb9Ubk!T$Pestc1ukF*yloZ;C#QEdsI00#pgsj>S_TN;_A+ z@zkuJIqyt6V`@gB6kI9PLzt6ObJ)I+uS&pBLzrRo2PLwDhmRIv@#isp5lmXbE4Wf{ zB=PjB>C+ZCYf~8s2@Q{obc4W8eicYaPC~EpdLs^K zl9wc5%R?|xlJF!6K4I%AJFmz*#YTPt)aM*wlts@(Nq`UwN2#h)9J8hVhJ%ch5iyTc zb}bN~OtmN!=9tLclK@)8&ZMfwP{l@OS)`;ww3HR!Gx;Ve#eruvL@EvcuoWd2PR+(;^^=?A+}`%$yzG(x}pMl<;T#g&LBr2W)hvm(!=Pf zOS4X*B&3Aa7q?_sCd~YJ%8|f0q~X*RN0PKJ0uxb6#H09Po#0}Mr4~AQ@d-GQe{yKPvw=IsH#Y~R@OcA4NuU_x~u$b9FD#-b7apdttK{#x7xtUQ++C|;b3SO zbVj;iYow4+g@jCAUQ_lo4e_+NtOfEAFR6%>;(L-`eR`&_X?=N-R@REFT;>pz$S_e$ z91j|~1PjPWLVix5gz85^9ZxYmffv28;H>n_L!Wj{^=D8e6{W5WBA*$;>~`S<;v*JA z&Ono?F?Em=Xy9n;$VP0%C#@m?;D)Vg4+s1sm@z_4F^Iep(knbg5)$$oWr2!3UW?el zN}iz?D=laA6_*gKxcJM!#7B7pArt6{Oj-;Pdq&RknyitZroet0H-k1f?7W;vk&EM4-HUkR==? zo;)e3RuEg9aFn7d(A9ORzLKc^$(miMI2%Dc+2*n<6&IRZ7#^8Z1b!YkI>ICufwIUx z13!7xK&gZXPmE7<(xTxaBsraoQS&yD>}Dt$0vXBIoT)b&p}L}B2-z94wjVQI0;R>G zF={wSIY1Z|=7EcI8}wXNTOb;}$3p4_u*-R^At-m7}K+MRQGA8c3U!Y2)A_BNz3O9J9F*`&(ms8oLAF-(Xop$k84$GKd=iqcEHZs9GrD zWoA>i;`pN|_FsRPXoAfL%dWWe36dj@dM}JTe@?}CJ$%TCCSkfIwZSVuiUWHt)mV&cl)L__dU7sp{sNe({xgA2PKX806M`d}eg*8%PyF{ckG8O4T3byN+Co>Sf$%G@ zMIRq}prs!Z{lTC}xh!g;RFt-p1%fzfvC0Z=vF1@-kxG5yDI^5^LU1EFRzwv-PGa+T z6@`EjSXn@yeoT~Rvg^VxM3m8LFzQaUEU9PazHV&n!zh0 zi-@tW+Ym$daBH`4oPEozBUyb2uUcfGbg2qYuSy~JG<51&J#a#lLSAXXREylxaO6>| zB?^3Pj8) zspOK#okOpOTP`iNPW4L_omsdX4Kzau_%hIVKr^-+^ddfz^2BeRU4xG zA%#6^_?#}}ID~&dyXD5OG1pSMf`e#H(Z1;9nn6KySHmjcPI=|XZ@6g`@@ELhMRr88 z1RF0_H^O^0h)j|Of)LwWcD%F0iH{N)BT$V7^#5Gzg)uIkbg zxFqqa-tgfhPY;m<2Mom;cZw!Tg=lD%sWv20AvzD)G)EL^O{1T1?5D0);plSw8dQ5} zJ9{r;WuZAZY4OBL7P^xqhJ@-Q{ZrQbvM}n^5;D1~1gVIpcW_FfTzaEv0kN_xh2)|< zL$5`21GOYDg_0@escqpH>IR;v7Fnpb@aGYwq9jBw3sqq_1d}RMeFwaJqEjvKmyW8t zhF-@8ay5w!A7e_*;E7B{RVuRzmgQSqLQ*XROJ_)N1e)ydSGPFCsbm>x6r4e3;~LnB zYygocA!uaBRa&Hq{)D3mvI}&|vxGoWoIu<@4R#5)B%|TMEj&q**N71z#pYFP2)u@K z5jClZr&=OpqUt}B+*8_MJd70RN1Wc4NW^?5=<=wWL5+@5-BYe$u%6jjBUvX4N4PM0@0^& zLy&qXW&A`B@ggDOu+lB{F5Ok8sSVs{O-6({hsW@QO(=UtnO$maS}UV`M_S9>T|Bb29ts1`lwlmR~Kb7G&`PM`CHqntKI;%@#Wp? zU)Z^N$5Z!jd+hElo4z}Jd|!~cZpsOBCJ&r5sXx42H?i-W3GT$P-7-!tp_&Ysk`xx4 zh(6~587O-WFM$tsyumb>5i&J_Ta2iPIp<7Q&+^2yN{fbz8CWd={&N|oW0GTE26Ykp zenCS2RqAVH6Rf@NuU`En_v8q4e-6CN=xiOQ>x0#mgLWI&z?fp4mCa4v;cxd?E|x-y zvDn;AZLx-hU4&&<)`Df7QWjvDzyyPrY46IyS+-m;jR_7qfI=yen}S>uWHJYlWOSZge==5Xtdr=`lOB+828X;v2-$B!VC7QRgWd3w!i z$a7E>`uddh{i2LbPDZj26(&g%o(T~p5Sdpa+Ikd?BHXkxv8}nnh0toKJ(@s2r@&B< z{vTm?0{mH39e5lR1Y}Ps4hx?c7>St?6zV$EvC$X$}MlhD)BLimkbv4VZ3;m#the#d@sHLN5g@9WM0Ytqpm~%SLz^$ z7a16YpX?f6KnOK`f}x=8^pkLy)UB!Bt)VK>eaNh09Rq~$wARY34MY(p+Z+)U{Y(E)TbtwuHmG=we3kO^yw$|Bbn4R=eGxtN`9kEB(ANW5OiXPfz&yir z|NA%pT|?^(jER(v$gInOQ3 zy$MrwVvtQgQAH_IYBkW*QWr$KR|QQ1YmvNh4W!|?>-vel?MK1|X+Q~qJlU@-dIb^y3qJt53lm*vSvIF_?#fWW7 zNGYWtt+9YSQEle;lx`JeTh&FN6rrY6RN^XHq2sG25r-khkCh1l1u=;5Sr3=oGM%CV zG4jx^PDMf)w}5m>wt(Qegey8MY#>O6iD;Cmks_WtD9ab@$QU~q5xC}nAmcW{Mr>vH z3&(_tk}8E!2A!m>-2y_EVl;3@DefVJ18sPVb#iHY%|M)vT^CdtY38YPDS9!tm%y#pA&pmU`x4wSY*B<%OlMgO={Qm0?-e+5p z@*(?fH%W805icLH?`#pVF!`8yZxuUBAo8MPN$fmIMa>}XJ|B1Zt~Q`QEWVzJmybJa z>qGY0VE=cox%WHQ-C^t1mlrP!`mFU1KYM@5Uaer$D^3Dc0i)x`=6nzfkk|MuQv6{N zD2NbJlZF}RG_QwS*_wM}eAxO(B_thtcA7WI|ZE4`RZ7Fn> z2S=7;;0oeu3APY(NCZ=SQE`ok%wrI z%dMh2IgwQhFMW&+nQI7;w74^-wO&!qfW@7Th)9eNh-T9iNdRe$c)lp9IJ6oF1@ui~ ziu7&$XUoku_n;ner4l+dhij}AscyU&VkAd_sAQI8;6M;BBa#70g^p5JDpQ=A{mG$kIz>rvq3p|wOKf`x+!Yap04)T&WjS%xGkW-+y3swm0mGYH0oZDruQ zsuE=tB}I%w1_Y(iQ5C_5X*DD#QV5bw22c@s;sxTCoca00)A9Ho3J>dWFG8xp4C26F*{& zGc%BcnkveeLvl@B3n*OWapw(6!)fOSf{QgMrMl3Snm%anB(sI}O&e}#(L|6Lt-mbk z@6Y1p4c1-9!%y>%NR}}%C7}h^{9wg5ji|*BWErM2{Pi)`rk!@gNG)~k9b0p)<1kqj za-D(j#6Y&VSmbM3K!nZhBY5s=zd%oLoZcj07Gf^+^#{WrJHklXk$pUB6OwZt5KKXj z^4Lmqb29|&lILvXL5=!Sb0c}!0Y?OprJXAY^=Dnn6$=dnZbHlQW=M;XVWqCdqDweR zL5w37K$94X(iB{k`m;zu=iy6h4n0ImiP{|F^o-R279>K*+t`9dRP(z+%v;7G_eUm>oFD>MXUP=l6` zh|4EGBJv<3spwe&%vhA^d<)|6VZs4d5e~|X8MU|o!c68^tCh46guqh4vbcDp5l0s$ z`yhZ9^S#B79U`upLkwH|jt)qCi7_OBBd-*@?OgVX>qpOh5UKlNK9$Qf+Wu}E2fEZJ5TL5syf2GB0EBzBg>!uFAc!)+xC zilv!z*U#Bv7D^XQD-|ri^ukjwJnM_ZcRl;monL?K)+Zjg;eora+GqDo=k2wnU|HaN z@ZMV=^xmxw-FvG-W-)T7tR1u#K6mb%c4%nGUUl^d!1sse$nVTD({+0Y)NujSXd!caaL@%!5j0=Ot4~ByWFL)}h>>@e5lMa<+=|GL z9r1|j{pGiAx%mVB!*#>8HkU6x_bl=9tv6hC+meNobmNt`F1f~5n_}-l`7pb>t!^uzRZo&v3O7`4z63LrbYbDwL5Wvs(fnn9)>`$jqZd0V%>O zRgnc4O)cycsby5s(T@Z{qdHSIzU2TGkjhmx5|fl z+6XotH4{?Q@8Jo257{8I9-~=Akd;T70GdD>nKAG+jTr1I>ZnW?n!M31*>z$%PIbg- z$FS&VN)n<|f6J$3VVU{>Ix_02$T+MdsaQWjgfW$3#5r_jZHZKX45$`-6=hU$1x&W! zu9`;Ux|WDUo>2vYCPR~ISjcF4@&n?|4l)SJXbV&QPz{6_%&VeI9R@Y@5l?W=U+waL{S~an-D7Vhzronm(p788#ym9s*KQ z7DI}HSrVj#QHrn_{49?YWhIKRl8N0A1H@N~2mvrcjd5Jl(G@$atxEcv z0H!0=g;zesob{NiK0ux%k%6xXa92eOf@hF2{3Rj47tZ*c&E+#r`;6DtI9hxp{(LFd zT1V%TlagVPA^;*1g095Ch45%j_ZCImDtu{Y`Sa35idg+Y3`X`>OY5McqOpTrak{8P zG|}!`9-ym~csy8-U4&z*v#KXo>e_X+syh7anK`=|-B??%8Q-HY5A!`UH|Q*v{wIYy zjs!N5f_rYp!*Lh}9MFhnI!!<|D(h5k&1ULSf%_rjAppKo>T7XmTpiEqE#iV%Gn+&V z4a5gPpD>gK)Hg^@JlDj~vZ7zTdKO9wzUj%!- zkt1CIsONOv*cP+H*rG))x6YHlt#RpY+GJiVNYbi-CL=Qi!Cb+KhnJC}^XvBHR3yPP zF``gsrg%NyEm8)T@mqBkfKLk<-3(tD#EA_Pz9My_sS3)}2Z4q@@l;U}gd`fb-cEBG zi79BLD^g9L6dW~7UU`M-Rb`&7c$qa+?-_#bU4+YSNEwExO(OA`8HtYgN;1hWLtrU^ zi9{D2uVFI1Rh?`kAsEUd6%aLuq>_2nlqD4n3m`~Z?6Rl4(m~D0)gg^LW5ggeWDFd)fM#H$Mv@6<2Kegna^Z8= z;^nUGCJT^@lna&t+js6rc9F6_Oi52aew&?Thq0e|@^<^n%a&fc$Bu7!@2+odi&?OI zz@D=X+;em9FMEsGd2D;f?J4)da+}NGlUJ4Zot)fm_ql*t=^q@m`-k@1?gIyGDPFd} z>`3+@`)<1Du5Vgiyll3#1PC0sogRA%Aw_u(Dx1}X5(mF;E+BWvI%N^z{)v$Vs-{`< zxs6@Zorj|0RX#2}{ zoZZ5KY!Ojw<=3pp7ZqJ%Bu)VlFghY7l=fXpZx|6w%)+M?>Ut^IZ;madklW9-KZ_38 zEZpLENyaVNgsu)bdlX<($*qu_oQUw5J?Ez^m(`vxVeGKmb`%Sk&mD8cg5S#jP_ zhVm^pT-?XLE;{E^7ku$k=YQdo-du)efBF7qxaqM*!^Gr=93S4e_6cT+F`ko zX^AmpEG34_n9|MjCU&=gP4Z3t({On+zq%v24Z#&&Ga`|ctq?um$pNp$aVm=GEV#sA1Z$Qx-P^(S2i*n) zQ;`v5i!6_Vr{PK+_*@RRoDJ;@f!OF!mZrpTb(`sANJSWRWfY;tQIT;ANZK421W_Y~ z+t&9uk_1zJpb+nn0}sSq{75y)(7+T&Rmh+SCe_3jWJ8|{#OMkWggmTA9zF>_D#>|q zYMDYIC{kDO2+e6^ezN8;%pH>uBeRc(Z2G5);9Lgt#%3IDFn!e(u8Py`2pgC}V16ka1j#^DgW9}AJWry0 z?~6-J0PRaE_OTtd<69@*FJV}KZ){tKJ#E%&Yl9bkbjU_>VJ`_0M}cM3Le>!WEQKeW zOAnvq#Pj@6*K&qIo&haAEQt%%@Fkv}>B~o|l}a*6h>>BUR!rTh z7(3PV?TFJ39CviUA-jbz!Wu#o@vPt#Hk2V#kfOA z7*+Ibd=;TAztyZ+LL_}v2UZI;aDfsnL}8xn0TQ?tRFpibgJcjiArl@)Tojf6&;Rky zIq+V9T78w3I7kj6@{Ty++(ijQlF2RSH!>`O*hz6gmZ-zp;E3ZQv6#W=DY{2n7C~Nw zonLQ8m>dii0%%G--=I^A5hi}u%n`?2K>>w8z?BO}jC@XRp~xLvq&C^?o0(S)E}B|Z zO*O{5Wzns^K~w#cgxEP0ZKRuyJg#>hn_wg~8)zx6i{)N*XJFXY;5Dda$Rp_zC?MGW zvO&Gx+G}wwAXuy+cyOUD^e)RcO*s;RZb03P1%CajzjL_7p%kpW*M^Y^kcoI2uHKH} zRq7!o0beY%C4s)#%p}9D5|Rt;>1Z-xC{!II!Xabq>UO(2GLT`wB~pK0s)*DQWirh7LY-bhno6)b`$XGdwOH>MSaqc zk$KfLzMJITe@}Q+7c~pM5E)TeLrkv*!Sq!V6;jGop1|bP&`5be!N`V#>Wy3kQ8Q|^ z%Lup#38EoDpk-qk( z(;Wg9iKJA;WDqagL3Rwg)7OG!kur$eS3Qqi-@qu=Sfm%4_$CrA(4;uJl}SO!CGz~6 zsajnzN_b6%0(NYoC?8uFHS`Qp(~AVCLmD!Co9TENWO?VYy|yg*lhjB~WlKWif@NK=7Z*Jvd1nP{+@W5(`#}T9Xqq+TUAyHk8s_Z*g2y1-Ariq zOudoURBu1mQAsC<1=>PdQKyU|ONNEahp@bUi;jrT#N{K{YrTFIyU8*R@Q99Yz3GNI zdriI`_SV^3`D%on<8!}orVS*JqYpns5Nh_u*AYr|1e<;eBMwE)S|j6Y*orKUHol4K zfvu;t))xV(xs;15ez%t-Mpmkdz_wu1%>k$E?#)!)mL19)uq>5aiMtG%gbJ2_HSj$ z{<5!R&wL8|?Avd?{EMGI=?kZy=)+&0%lh8e_rLS? zm!6-z4``J!QM<}_YatucxV*7rp)sqsPE`al4ez!U(9Az-8HNTU%viW83K{T~$%1I1 z024cT}8z#UJW?}?Wds#ry z$6eoO4-uMf<{1&1i10254;d!<1b{qoFlItj4uEAHBS4lM@<3y*D66ZY>QoyBM_Ec{ zS9xgMB!bDCyU>(JM~p@tzO*jN8}SAo#gSrR{#A<9i0DWdk(p}piQy{6f&eNK-k60J zYxF6CCLE>UB*})nDGwYRLFDHfxgMkazkm7-@02H5)Id%1ZC6!DE-#_u$}Cv8B3@@; zE)7k9fYkIeStFWz%oR7}`=o$DkxRxeLR0nHkr>W|vvY?8=#{M;N6~5?Mw!z@H+l#m zapZwJyrHf~Ve%Yo_9VIX_3%XOZ$$I1nF|~>RhXB|gk&qOwIPWtWDl|!KgEq)l5?~5 zD512eQWecjYof|DxatqCZsEgWXOvw<0U#LxoIz9^-2+@L1{@s^0wN@&8ZHnZlNMUF zw4Gb{)opYr3XYVz68K0_Qw!^a2^mC1l#wE*9u+U8L@kl04xqQ_B*I253jxjatxAeR zmaqWC3qc0w6?3HZL!Au;Hh~jUm=e~2kjaCox+#T%TfEp=6}M_7Gl6k*YozEo+;|9V zVwgy3WjA!xot|3|R||Yt$t@OS(>X;Ml%*hQfC-?;Ok$E}foh?zNFswoerjrNxfX zqn=a8IW!m~{=eWtAV2`_q9Wq);@oz$6#|V!ng*9;8W%go<^gLit+8fI(&EL7^GIJD z>LHa^m5JdggtlW(IF@4%ML88hT3=UyOh^-jtC$g;WHZunVQ>?U$7OT7ij}7hI3h8#PcpR>l}X99)0XLqJHPmIhMA zD5D*B$ZDvk>8>g&Ld57Z^eCty_|hC{{Y27C8pU2JPs<*7ufx5NgQa3KtiR3;vcKpLvhLSR7JxrJF*M}%Mvd{vB;MlnbO5ClPnB%;TXZF zp#uj&K*_C&OvgC1me(7ptcwvnvT!v49tzg-JD%8@?6-nLFk*2If z9!Vy}Wg1h->9!^&46-c64eGUM)<$2R%>_H8(m;(J83KcJYc$EY4M@%buV~U#M2igiel zvdPD~)65Xi6$Fl=;$^3@WqOO*p0cgvPG66ZxmTFI#q7{^hp~mm)8F;7vn*-`aR&Pb z&wovl@rmroXR*I=w_y24-+TD^Z+!W|dlos5ZFBj^19lWI+g~nT_Hk^%vV+-^SCS9e zrAS$fED4S6CWDBV}6hG0JA8l zIOGwkBgG>XiV8M`&)zd#Wu+Cx%RQVFFI!^J7i?PW#HZFu;jkru7OF@jEYtaI6C*US zCro5z&=->mZ-uVvirGCSK+{IHrP(aa@3nY=U}p~ng`ve zS&7&Oag)65Zz|J;!98JcdaVzac^Z%P8`ocZ|9$5WkEcy4TmVJ%-U7pv43rh5^YF#X zJ8!$SmzS@({E{V$FTZZ#B?~V;=k}YfUb6V&8?U}ZAlzYWpZxN1>?;-lFU!BkF2O9|aAtgSePj76$a z`I3RtBIvd?D7bYYh$@mIw}6J2;buXMRMfzim*A6ux1iPFSX`pth;}>H-!Ya!20L6V zf>va5@^I+l$}>TYo5!}|EUuJI94GBt!Ld_I9y_(P7c@^x+A);xfG*dC8(_splQ5*h zh)=7!OjkCP6g9oLqM72Y^)MM64q$MSvkCF2jJR9oRb)YTHGNSTzAMMTkp?}Dgd#pN zpszO?2%H}ShcH|<&LL##a4JBIFW0zZosL*Akvz&+V?<^ua3pxDiU1MkHmk_1G-|w4 z$rSuaraR+vpF@^w%CK5NMPg&BA_YM(A|8lT!=aB0jtI)&3z?HcNO>l8^&=`zjkJy! zcC$@3e#3fe`7f%+P>AqX*#{231?+?oq8b(iW+knR5t^QIDDp6ho9#iC!Nn$g5=ORt z!&by&JvQM#D>4Q-tAcAYAs?8t2iXQ0QxHaP{OAAkzwm9&6cHnC2pi)bd^vLC(V54O zW)Chm#~3@IG#2cjHHlQuq(0qU-BO=0>_GRrjm84W4s62sAkd@?iw=U|dIibltD;tv z4FQ>msN%M9jj05pNJ6kiX%M-()fu$4yiyGzMK)>KfnbOL4mk;cej<^FRP3f{bQn#5 z0-B6~FEj}SiOTdLJuR4x!@N%sp9A?2t6?s7Pr37|Myi}{R(S`7? z7%5V*o`$Dj zdBY7H8RdIB?aafg{1&Cm2#hLYCMF64(<4EQXQUBtJ^VLTqL9$ogX^ zoIq1!)*vDz-QCtd(Nx{oDchqah$ zT4!Red+vZ>TpPSS1Tgs+#asxEfibRDGb-cIPO!{A#9#r9sHHsFGdG%k!R<#|#0lU5 zW^y{vVj}Vkkf&twB$zD*EpBmVOrbIEjx*V?Hk0sz%tEFYOm$Qv0;Ud+b)Az{73sz- z973H0DXjp9U=RTHRjE-!Z0)O+^)9{^V{(VI7dnxJCgfzINoSk+>a|9Z(Qtfqk)Xra z_LsFCpj+*wsPMr+j&&GSi|WI5wC24z`VEJI?*_{ADjSD+0kWMr6Rc0G3UV zLLa2YOc|R+FX?eNso2WQ-=g z)JW&=36llOURai7#{Z;b`b72!pWAVsK6foT77^yeCvQ1A33_dLl5wD;M@_pwhr za@%7L+{Afo-}~Bflh-USUKaY9s|CMiZ*w&t(#**Z_j0H$9?6Op&4l6_wH#*l6jyXWetvTFt!O-X!6pipDz2{#`30?3 zPfMQFL2$vjwH%O8-0IeX=&7%z6DiA_y=gMoEEb9IVJxQRYJRnKkotDgMat}BBiKU2 zbR-lqSOGm)z>8G8Wkw)|&36KbYe>#wgv7!E!Kx%-QV>EiqvsEHSVFE?RoyH4Dx;)BmKtblsIpZ@lW(8zv{Q+dlUGvi;>N zFFy0Uvp*qNb|m|ZQ$M=k+*7Y#bbbM~c-i~Qg5{?lyZ4zV9&jF8yzEFe@yybaL1Exn zL9E5bNUP5nH4U6GcFgwjW_Sybj8SIMG~$dkNvZK=dCIjw8IFP33Mda#DF`klc@P9K zf@V464_L>w1$;v_n#jtiJ_Jx5zH1PRDLUk#7vBaxC&4K=QH*BdUwPk+>!Jm1L^^b5 z+2x73da^-%E2&KrPmP2>JTXuEHJLsGM-g@45ON5Q9Z~rwGO3`bd4@<7CzUExAPEQx z1XBe^d8AZiaAwy8fT~lL4du~M9a>GJhZ~Xv)7f(Q^h}2q^xKX`=;2zy8012r#&n1i z8-3Jj1=NBo0wjrBOlzQcxm5*g4jVcGQckk%4^xEASx*mylT3#o`JvH^?rB|{0wNek zdCkEHiufwnm2j6p5P?i`!mzfn?~tmkH}0$UG7PbULob5aTZdy(^3^XH5eEr5?fL*Y zSpbmRsUlZap8v_+<*3*Y=d2pP|M2&JqyOORIRH1z5RPE@ItHx`NkE~xcJZbpdp4Yn zdeJ)i5`(}r-ifNic&=zlp|a4PFnTY2$;PCvB_<+?#s$gDeGI1uXPZ3WtOQzy(2%opmH*)`J?-vJBlNXf##iVYP}Vr~(162vP))3`Po4 zgj7mpK&*8<`==IGUDpqD|Msu{ic7{L;%;$P`P;dmVpXSV{czW5SDzC>QzE1epmS0m zWqN4{5J{FAABm$ZUBs!D3;ItF8-q7Ru~c zORTE(RIk*nhSLpLUK0>fTc|Ev)t&TM{Ra;BhC$rAayi(LZFy@37pt@}97S@9AWm5$g0~#V-Pul3R&!^inCb;@JXcd5KL#BGH+QxZizkEER~9%6f=Ajbz}= z49dw@+Kf$u0>J`#)I`l7{$|aW_Q*m6gg8csi+zT81d|dH!gM5M27smr98OgNx&=(} zq2R!QIU+AXmOQe6)yf)C1Bw11G7SgJq^cvLD74@N0Ey9HAC`VO}3W=W(hYPPLBVrPf3pL^!vpM3vOANzV>+120u*1a!0bNAi1UhGR>#~-uj z``)|te(#<&clWpK|E|rvp4=&GA+ud(L9vL~)l1FNbPz3S7D3yCKI!N^>@VA8w!iF` zQt#h?mhWSK=&)G_?6vmJ+q`Dhn^xFlgVhA2{=fX%*9f^*T4BW%Uc1`lUhrt{5}ny2 z_70#YsODsgh=-+?X%8L+bV_Y;9(2H5leszk@Iwv}V2a=@E<8!q#c^A2v4x^_)?CA! zZ*G)Csfrv1^D9#6fc^IKr7wSv^8ZI~F?S@}7cc#1%8M{U>GjuM+sDCh7fW);QIkX{ zRxWO^BTonlW348>%k3(V@$gnCYDrpab@BH~B46{QC|Kw!f}k4tQB$#9W|59$b$}p4 z3-aS=nx&449Bhws)k4{EQv!%VtS`Cc9y?ozVVaZGH9wAt+%!|snXiSV@IC=UUL@sus=5dKKDsft_etJtD4PwD8ZPj*k4|M^;H*N zxQi;nREK9VBqk&3E>pvbV2 z2UJvDc?%y>2AwM$5X2#rmq8(9S9uV^pu-JlQG*|s)=Fqf8^7`hx{V_NkPV+Air87d z(py+npvPsh-QdZL=!SE`9qI@rYSwNG7Dk#r4~H8jqaSKAorF^g$MVYku;SxlwdH02 zTH~z#fCT0RQ8}fkAQ4F-p`;csR4oZRm=M5`a;qpYQeUaWuTxZH=cW7g*O`2Fz+)vb zn#02!JcGqkoekDqd-J!vML!oaB2F08^e+8YZ|4PZ70AXWu8Qy_3|-KNoY>&J?IAK&YIUDkuY`n2S z?XN3hRhI9|U4*0vbx3Gv?6kw=&Ztk*U#r1`00^|oW zAaC5>kayuV2ZL~a7*LDrf$B0oUw$;hb~Fla*$kuW zf+$raRSW%wh9VF&B^hdJGDR>6Qx%yKc?d(!x`eJ$e^#1E;7a+Z>;_pYkX%o|1x@rp zgu0Z~xQMGoFtzH#uJTlt5YegJ!a-k=&S^F_VbVxmy8yC7!b8^XOsy_pVnE1TX@?G} z0I9DtBvN)D48_5fVok^fh3=#lFH|lfyJ#^K_^g#L&#+5us;g#7@AV$LhM*%Vf z93A~NFpZ!!S9{&uIdkkO^QAe}oNX?p*^sAgBTQVtAy+DN6*Bu=G7B>THPO+aOd2nY zSVqsGprn*ZA^TLmr^iI49cxGgBc~#2x|OFC*@&$c$4tUkyewEIhCF9afK005Wt~G@ zKs+&mv(RdJ$yaAsW3^Sy06tejL36KHGxQUA#629aKAM*rw$@M&*~}q~Pylk?{P~JR zzNSjCt*Dl3!JW_|bt*Q8kDbx9!*<)4>Mi<^EzR;5oO><{P^30>Wf>MATb;zhjXRnq z1e1noS*Ytox7K7pmLvwdS;2jW0fIef@wJEK+y*1X5Oajv%EH02b0Mx*dh`)T5YIe@ zQO1NuPWgtFQpAlBlK@jQMIALE>1ArwQ8`6eK!8kOis<>^93D30X5`Xn_lB@5%7zBA zd~czIhLE(8c-aFDKRV&F$%rmBAAeg5y24~bni@V`br@t(J^1HxXT2ADHG1X1dKJ|3j-@s3w#it7C1DdaIb0P;WMu)f<6e~Zfikx z!3Ea@hNe^l;evRn>d+La{xZE_LV#ICXoyHM#l;ZZ$`{PK)&Yu_du>^;EEOFWI17`1 zVMhApAIpfN0dYZUkc}Nu;$>F?&;kOu@D^}j;#g874S|ONVMa}+lnSnpQ_cP|BJ$KH z$%Np{;f)4wT~z-XNq3dp)`1Q4S4PS~`aZU+c)28A_CG1_FN>Fb1iKGqJA*A)c1XL} zS!oa%=dr*0t;c@)(&ImV@sV#o{pBBh@8NGhb%*_Bo6ACG@$&wAZc)7KOJ9BdTHq`M z_Wp9OFpG|hooza|zwBz$*)0TH%#cs;rt`kDy}$gSgJvCj@TSgV@3zCL+i$trrf*z* zf;V&&U`C~7F0G3 zTB`b<6^|V$tGub0J8kJX`}3!Hd3lXhSN4+fBO=T@Zu1LfXTy#<>|pEH_SI4JI!s#(2>-EwKIq$$@07kNy@P22-vuanZWMnnd8bda^)p<{Wq zXj&*)i~wy(3)3vq=(7n$Ea*aSODc#rvMr7RHxG50x@Qr66`WOh_(+-974h@M%Nwq} z#_6B@gpm2XvrfC@yt8M1CbiJZ%af4#>IJ^^<-~R0`&xYY7w@@!@%%GB_K6b?cfI_g z&-y&|SsLk9_rZN3y^6@K6CrvHAPEXVS^?VZr z$%#Qh$gUP#xAGi0>u<|CA~IBy;7A}+D!?yIn~0Sxl;}tneb?}t!HA?Yh~fo?B31(1 zp>QRbWP64D4IHO}5f@owD#OLnKP8Szu0G&sAoAc#49A#|(XAtVX~ZE=#Oo6J3{vJ0-GeavbM`T5aK+U!s1a8S*|ernjmx&#gtF!w z4Qf7NPtIPwB{RAjI50<2opEr%j$bOzl1${Gf%#b}yN$7_IwKIZ1T%baR-%RR0$(i< zweUh&A}5+`qB%I9W*+mBUJaAwUA0{;UIxAlc-7I-E;?z48QU%|Je`?Ch(3BX|cZ)I{dCIyfDuW+J8nP}2~ zW?tvCUnb(UJ4pZ#o*_qbU!O>6Y^tiUla z`dVrUCQ>H30f@Vq%Hbl&o-z7-sMXnQY5CT%(I>eE(v)aAZDLyB!1&}f2g6csbXyU$ zmxCp&jhzNfD&0fU$5;bU3yQ~0Q04@Ff{Ki0&sr%1OpU%_WOMH*eB5CeXrNrr?JAu0$=8iD9wTF12D zid4f5HRdG-k)(PrWTr-+O>_VW7wV%qY0a4cvskHUMF3I^;+Cc(xs^mBAghw;p^YjA3hN zgF{dhBfN@?!jT6Vr4>ly=U4ukqP#E}X;X4V? zeiv!NS242Fd=oor)1^sh)?yr`Z83)|A1Trn2oH9H@(@WCDZK&)iQ@=4yNx~|u|ZH% zrCS;HpWa;d+VUhKHWq&Q$MSvk>ra32N0J?9x0Cmpmme>qkD&fzV#JqH!dqHnL^}e2 zq_x|`Q&gIk6J}aK0}qqF(M%Dw;Mi_9eoeLwVf|3I)J028bb!W@Br}sHe_MWK`VW6* zxLG|N%CeqLUTyXY^AEoFP0_Jv3>IEfcH#ZUUnY4FJlXqy}vwpCHaVViiLgh%Q0-5%bmft^(@oN%XXhd z%@Ck{45m+K&)MXd{Wm#s-p1nPz23goJ9pe*^NrRNYKfOun(Ql2_Lo~Sp3Y(g>9n#Ec~EXIW3Ata~HuFCt5IzsSh-s7Z+7L_{7ZN7#1zWw+2wAtD%Z!r=| zBQpQ2&kKtETlt!+t`IEyIQEij7ki+6*6H2`I#LjQ|5xsQ;NE+L)mFNjZ@5+!Q74XK z84#N|2hN*^QQNXIyX?3l_!gUO!aXLEn3UHFVnc`6!c~!xsTii^z^&L>-dv3=uTr2Y z9G3S5GaKkf9(I_zPUMP(sc+dbdBTxA=Jgk5{s2Wp*&D1oiQhd#wijs&$&LWs(eZ@A z4_Fl;s}^9NQ})CFJBMav*w%))v$C`Rv_4vqt%-KNEL&b=w#ZsTX#|1UsZhKxh!zmx zVnJaOlviJQ<&D-^^N{`aS-9YWbIv&R=4*ZR>oT9n_VZ<*{3=-PRQ8S6T;%livOBK! z=jBg-{Fs?few}8E8ICV~`98M&Wj~X8?EX8Sc<`S7C-vj+Oq%RSjZ zU`9kH1ULkMHW(=av<6v8J)LlBLfFfz;oCT+pdI-q64zzVJ@$D0D*y5pzfNT;U~@j0 z$NP2}?DBi2p_M=Uf%kJTu(K#zWGPN^Vw2OgRU4dOsX*H7#tykOiey^WWmHc`qA~?r zS;|SvUEwO=k~%q@VnhD3o@sMjSDd}oRy6Wnybi20i_}QyI&bb=b#q+^gBOc|plJz~ zPb!jNmm(~N1!Scl5VX5nKvHpCV8Y>Bl9tT8O7x>l!rH=2%l`6Ak@__#U~_EKF9Z}2 zCyWutUy`9RgUcv_W-L3o;p9BOmq@B}{(EKxjpX5rVTpR&hd=zOQ%+$w>=dCO!%ZSK z*PSF(kn!|sw|Bh5SdO60ccchF9vM@XmLiq9^3KUHru44ecGDkpAyVZ-%42Grnk}Je z6!AO0cELT1q3>(UV;*HZQ$R~1o^lxhy$n>x4N9QCD z0f=OA*Vc6JmUd)Qu?1b8m8cKj6)BjC@RcCL)R*v*Xa@%!@QkSzgz8p>JZ@P=E%Fl> zzW(V-aY3Irs>Jr2HYv@Td)sEmqt)&Pfnh-~jN3cwiocx^sNi6q` zrwGPNEP|$;_BC(}I#ZlbmyaP&7*2>%d0gCjs>a~Qf8-+uX8Y`96XY_HKo!KEyQtYz zZ}KFeiJdOQGkl0`2y0O06?(hXQXZOFER0ZGw5-Iu41yQ);!7h&a7D+hvF~~Q!S9>v znJUYB$nUSvEQTh6YC(2XhKfRd;jpEjUhHOw5krq6t>meRuNIHzb zM5D;-FnSA!R^xc*y>q`g|LSHI60*NMh{QFbj%mt#OA!$L$}AB_GU(gzCGyh|ToY&! zDr;6+Fh0qlku4l5{4Mh}NbtN6rw5E$1IFehb zt_yCYG^f2XQV#Oxzc4*c4r9B5IEnp}AKFvhZUxH%Wb)_SwkCvG;q|8t>h89siS>weec3%v-i3tbb`GAI78*F`)1f9r%!f+3j_?K7;!}wS=;-yQ zPdWLZd2{c$^=7XvUw-k07oPh?2eT!+%wN6du6yoW>RIJQ=gk+Z*lQL7BQ9X{D7X`| zrdg~BiJjQf(MPL^DAjyor6Z~UR*Zn1sZVH33>3tzyioWO;Q>Lcuz+OlEHt1Ms)y1d z4*OX=LyV}S7N?i|st`uVY$3y!GwkZ)9&{8+&&2QBiM0WF-mF)+wQh_*#hc~S`)s<5&Up=wngX}HMkvZ%96wZ0Fk+)rL#BNWWoHi zZ7x5!?2fx`x$epZ^KV$R;JSsriG8l$MO}B*1x{uApOpVE-*oMTmtOGMvraqS`^!G| z<)voPv|xEUUiQ5&N3xyAw!dsuZ6H`v%-a?btB-}r`r=xWw_sVJj2g2w9961SkvU%q z0-=>oU6^RXu^@uGf*=L6H4a2+XfP#^xC2(P&~QP3#n3K<;r4Y#ectqO~m-=Fi z$3(VP2|TlYPCiN_X;xQK4W{^#gaCjPbgP*NiJ*+4+L|!9gCM9)@d7~^KEWh;yVy#h z@#{>j)X`YXF4dHP5M1+Z(g+Jpsl$^^VmE%BYrG5@2jJnv%kHf)>)8?Ad0gB)iohg1 zB7Kb(X8sbUGUQZBBQ$x-HhPS1&1%brU|eVjAgU8sJYpo$J@whsJf^GgnN5i%H^?}; zvK2c}nZebyA%(l)GOEu#LR%Wq`z_UUCD!m>KQ*| zx3pl2`bo_Jt>+9%Yg2GIHOjaWtOO#1g3K76sz?>d)M~D1>U-$hnPO@^2M`*W0YNB* z2#s*mQQSgdyg3FK7%PHjDPi-`n5sxu~yu#=Kpza>AQ z0F8tpgu?=)B^3+EYc&>7k!qwO8;fNho)`&puz7tP-S)*eoPZ?ZuyF-N2TF6o`E~fJ zh&a~%1z@3fpv!OuK^!lgP)jkX1~SGW8;=;Vm5f@rLm-kO5_$^9XYea_w{+2(Y(-ku z7C}QpM=evRE{A(G;ZTj5X{REbSnEq}aUp|@CgTkF%8EBT08y*UkyZyV`4G%EPat@_ z28e}<_)4niTp*+z$iPYhKcLqG?8I3N?W-~G%;i)tPA(vh^0jHEkhqg-)L`vmg411)KUVC?jE z9h(fSN*KJ*CtHeSLqHi7H3~VE!e<1un^yXP5@Dmu?pfKu=qUq)s#FfgABDFwV0UM9H zVqMgblyq9%Rq9re;c567GRupXMam9fyA~{aow+xf+h2ASFZ-gG&{*E_Y;P;uQ1%vc zAO7lHWePwDDmN+SP+rM~g*mmUVt0@6OILe2%5P=5W-NG3N^m^8mf-Vnf-N zvHd#Am$pv)=!bTH$Ic)5z%lleMaSM{M%S8t@(CaHrqyK&F0lP7Ko+E+?=?WNr!@k1 z@hJ$_f>}Gxh=+!?bK`z`$cr|;02V1*Zr}@75ma4h%-1UJM#1~V_TYGf!gGGOPjeUW z3V}t_NLf-r_+}hwH0g-i_OoYm*S#-^xQYlOF5p^k?KL2K7LC3z$^&pG#~iU^Qoe*J z*y%6dHnTJ6R~PDLOkNOMS+l%X3*SE~shZ0X|5&bPVj{bjE$OM+zwvu{{@fspyCOV9KtDTlFz%om;W89?!}(&FWZ z?_KJXU-p;#rPPbxp6nb+Ea__bFeuFDmQ0^8+!kZkdS#HUXBYlZyhr(o{S$VuzWDUY6ExwBK zrE_mcNFE7}L>`G7pcJxFw}?=aoRT~b0Rm)EgU$~DrmKw)3#*=KU@9V{znl=p&InJB z2gpAplDect8HE@jC@IpcTwgR3#+7Pts2NAM7?C!4(7{pTf`p@#St(hoD3pQaH5ST_;lo7`l0(EHTiULKn1vyd3bU2chPm6`| z?n#;nL0%O_W)FdnAx{|2us(yPgoRSP%%75J#Dr;zGw*RMH7*KNBNBGnt0u!Jc@#lN zM@1PEPzFMY{A4bvNOCO#sI*L$*Ro-{g$xY>)tG{sTwKO+%VT0ooTKK|Iu`%TtqqeB$tA&Enb%c}3mIxK**~g(5QaiVGc?iz1pv z9w}lgbxR{ty-|Y#nslUyB3Fy4`N#1U%d3v0TVkO|*lVTmywq7H&u0ivhLjrWXI1tY z)312|UlAFamtB1Eum1kO@}>khJR;7dz&ZblYs3fRcX9mqVEk}f%u01~#7Rg_R&~`h zdP1$K8%Pj_O;VioB(kY)h9?z#x@(akFlvO1Dd(0j`L{-Lftb^PWaEF5Dl{G)3M%5l z)E;b7%9~0?I=CU`N<4$w8dUWEHcO>S{p)uPZh# zXhcDYvUE@VAg)0a8Cme{ElPl_Ar*%z4Iv7M6M$f(q6=j+buuSgnkFFyZRH!AMM;K+ zsmj2KJEqLe=2}M{Q;h{85lO;yNShdnpa}yQtEYoSAqT+Q0#kh9uxbEy*h)4Ea@ogCK~!X~ZKz5P7ZOR)8;$ z1p_jXt30XHRRlpoFuth_KSR07k0P0h@Y0&Kiz=zCR4m*lvW3htuHt3E@{cS`78K)j zW&>KrEno@`va!Pg%}f_NdGq9C|N3_}fiPWb+`=E#Gf0q^T@!d16DbrV-A`(;m}d+t zgVfA0x}{!fL`!wo<;BZB@MSky$ZUVvN$lsp&1JF^_o6VlHDWWw;Y(lkmk*u0{So_ZFJvxW?%iasBs*o@ zE6lEfVi2hq*-31XvSgPTn#lT;BX>K_9`sDS>|=j=(dF@sp zDG(Mf%kcNwXy&{L6NJxVPti0NUBGDTL}X_-goDQe6fLSHZZ-Ma(t>7^2S>+FjX+fr zehP|3#CDmT$M&n`_wBcjC2!yN?&%enD=%HJ_{z&}y8ap;0RvHV?V>A1-26s<5!T*f zCI)KU1`q^^N)kfKL*Qw+aDSC<3 zVJf`T0u-lv4T|T36l<`IK-fD?f-J|U`6mcsikiJ(5iYo`J-jTG#O`FQEon4S=JVLU z#?%%khZBAKR>Eq?=-Vh1sga@279oX#Al-v>RV4AUTJ%y~u!*@dcmAKJ&B3`Ja@Jec5sL{<42Gi{raA;;x z_)0TKdI2I%(~8uRrxz}i0z`1w1Q7&{0>Pukg2t;Po;|n@awrAVzBD2OMuHL5wN#3L zM-X>1B}HkLsJU^E14GbggSt$A_+cB zs=2xijt(;>fV$w(ge;A^6eI}*YNDWZTwzM84DrZM*}@+{RZviiMp{rJIU$)MQ4xU< zCKhS!Hg+;0tEh>|8Nry!LtwNruJR}|uY@DP2t!#drM(OREepa{np2fA{M~U^ zT1xQcI?|21#YN=ciFC9gM^WNof|y4ja3jUku4K+u*^}a!63JC!J$xa!LeMDszguY9 zLWVjzKh+3E3&0FcWf{rbArU5rIqt-eYzA}cxS+YduH0SJuy7?qpR8wXu_S_&#SshE zs09r=2G@v+v@AFY;i#_XkiUv14OY??q=jrzU|yxPpcl$oSzV+sbqk*i^wm{RgQG|W zaa{0%jD;(jifkflSre4HJ;y^3g?cF>IFiJzZlje^hKT|hNTMkvVKRAiD%?#()=fZH z?C?dbLRJ@k!YfteR+1?XsCuoF zf>;2irhJ&_$e^h%pxco$ks=7Rmf#Ru&rB)m6hzRPvL)kaq##TYLJxn`Vhu$ZL=pVr zn(RZ2jO2L`(390lQ6v_k-hf;Wr!8^?;wBoO>gP=^+2O|~969Vz-NPsE^9Ce{l4Z<`9c?c<( z46cCYG?A}qMCy43AWh2?n;AWD&{7DZ0AG9hT*-sRl*!eMN$Fwfg-ORq2= zy3g!G=4|8r<&In1KDLi6C@xGE7rPy=Eq5?maNS{SIFrcwfE|4A%m0^;+;{TwvUqv- zx2?AO&TpK2?`sl#z5IJ$!rQhxIc(Nz+sa~0{+qDTPOG>{unb2~?C>z4V405xnd2n3 zg)h8>NK=^X0JfvozUAc~1VT0jaQ594ZOP!9|WB!eo9aUh)=DQt>*RM<@$w_VEO7xY%V*J?dnMO zwF{lio}9`*gxp{zYLr^No2a!iJ ze;~4v8fXJSMip1=cozhKiF=MKBF4B!;hWf4Vm`d3haX{4M!5P_Y zS6pNOVM3q_Eo_2>j|*1>4lz)3mB$MZDS1XHuX;GBfulpmR7OMs#08E#9Z}%Bv0&Sz<$T zB>=Jc!-zXKVzB!epK2i7? z7C1sfBwdJ2DpsNbNO=;X!|XCNwYtHVva=${5;+0Y1#t}rTrHRw0ix+PAhVz^?~1QP z9vYF>xfhJ+p8OD^T;G5r(UHZnXEG>-S&~N?Uzx$wZ3$~|@}Q#|)V^*BS@qa49R$Qt zNTV3ZBd)YoPPI6y4qo5`2R=CZfNtgE%jRYSJ&ILFbwzrnuC6*c)ry+_X35a3nvCQG z&Rh@(hDFk%&wz-qa0r;HtHsr_p88CO6~V%%x1$dSvet9}N#yG>5rTvvQZxyJvb+Rd zyOUWMXkoH=`7*=CZ+xS7pACLak+YgIoXNlWtN-NFUOXrpE8HzEw^B*Q5S5`9${1;4 z#w0KZF4Q3d!A759sN~TQ$p%3Jq(zNFPIRIs4~=dH5=V~1mOF|m*Nyrh$CLX$wKMt)k5Sq8pFQAH`TwPtpJ2?9rgBT~jf9#9c{+=)b5VxvsLEUfhljdcKU zyv!A9S+-Edk#@prr@=j*%5?B0IFd|~<3)H7SNYMCnO!X+4TwgBfHJK~rBM{gyGlw` zk-=RCH3GobSV9aBQe;?4t7#W)REKc|)M_;)e;Y@NQxkneq70~|Fh!8QW(Ik!hUwtW zJ}l!3M@7}qk&#jvTq&i92Ld@uhE^$pZXvkER0h5T__Bn4!@M>{5QLD_(IKZb0x2M^ zNNLHf#4`F;eb{Z`OhGh5NygeVH4R6q+9Hm`AR7Tl0z&L0r-Cjg1kU(O9W}ga1;z4) zHl`qwha1V}TA-AWDpo5=3@b^KNz*Y@!dEwft|6$XR2rG`lL%{u*EA*^Nl`?mSqyk| zT;UKhD+ygu8~94y5}y4Bccp`}x+$VmUPG{nb247~Mhe*iWM%<3;ra_eHbi0cSgf0l zQY4GX;~tL8;3(r3JGH`BS941lSqMD&ikJD*H1a2ZC?koT<%P*YX<@P?<932D^+aXp zIIVcHk)&3uh(U}&Nd^T{MTZd*l@==MGp>?b(ke}O)PlQK9i*fOmh{r;WN4UhKy=i) zXajS3@v`rId12WHz8uN+3+5MJa9qtBj!$_pSvV|Swx=vimM>&(bGbNLuHQ8Slui3*D7YhRgN;2G3 zp{0ziWLF5BJs~7Cfu37Yuux9E5L>B;+kURsY%TrbO@Xbr)#@+k6Ku{u`*fvlZRw(8 zr_~21g}qi5S8pQwsuhST5X8K=BM4uf8!1i{=StYg1CkR{jsg1603{g}F)A;<4kDwX zD&lAe0wTw}9d#xu6~xhV=j>?}mXgXxw)yG;UW|}ZoeD@%T!K#|a*~#F%M@H1JH-^C zIFXdfc(&kGS^LX43i%=1Ccu;y7D|kS#v*aAej^Sj?`26c*zD$gp(Bb%hub4zXECL$ zEq*vB9smA2m)@}SmTT|6^QIfGzw+`+&T)I|&CZ-(vUs6g{wr@@a@EyWEx6*+bFaMY zyhWFtC0_pQAAG=9zplFUZ1J)eoCV85ZJ)^g>RmVaKDJ2tTTeaw{qKDJ-~DiM{*zJ! z;|CfVd=F8fsaw8D-sy?put!Ur6b6A5uZ6ucO>uOfB^9kvmM5nKEjn=INrhb%D}^a3 zbqh`aI52}ut2}H12nW6l0qiE*t2Gz#cz^w?zY|f~fOCX{56QtmhX9a=sDlo6k|GFz zs7*@?{{Q*94=Bv4^3eOnFUA{hjGwU~y~EHu4806x24;X^=%AvApfPIHt5GpxZ`eQ- zMUf8DVd!OsGE}?f#uRHbMpM3<@8+(%?p=3%`F`*FXLjB-#kHQb*K?lz^u5nH`+4^{ z=XIEG5QiD1P_Xy|sc?v>mkItSf-9hmA&)1bxA1kSvIP?#aOb0-Cudtw`IDqx-~mQeCXl(AAjQEr=DJ+1y4Tp=wpvRv~2mk=np^gz$1@7_~?pd zD;`^p7z1144dA@xOj81a&TRm3$Y^a6;(330mo@N8S)&@cR+ za~nVJd-BZ!MsIWrjF$RhY&jtV77|tzx)cQ#S6cCEEw^TU<{^v2y-pn2t^~0D2QKeg z&WlK}#QD!#v0y_-f%{#>HQ#R|MoQJ}GgdZ{eg083XIbLnsiuHvy@R9v^+O+RU2h?) z1D&iWuRNmQX(jg;?+5wtln15wsg9-!BEuY@C-+2@hk`=1n0InPu$g!q&zH6MOZRASLmHkfo$WW>ZeWrQH(FQV^B*A}aA!%Bm$=w@$S=;_o;LsoQkXeOi(1!ogLSIbMkEvQO3Nn6X{ zW>-5$9=80UFX8(qFT15~e+J!>Qmv=z{O;u6tEHmyRvv&{$+`6>dgF))mn$jiqtiST zf^9OU`M{G_!tentjESTc(hO;mlzD?SAPP3qz!WtfoI!{UH>5$tPY*Uu0>zp$!in(A z;|J4?InKxi2c)a?$~KDO7=Ct6)yrq&jvt6cwku(%Wh;wpnS`b#PgIVA;)Cn~NY3-n zwF19%!RsYh8nuWx%O@o!bJZvjT_PQMh8J@DT2?r~?KMInn4 zp)RW6iK7H&J5Mk>%qvKz03R4CaDom&em0uA*kuKbq!C941&EVHKrkiT01;PjnMO<& z!Xhb3hW=F$oOzBXTa-J0$CW|C{pO&E_LU^{vSd!K>eB+elZ$?bvuaaSOOF_t;0#Vs z2G}J6Co8w5m$w2(k6BAla{Lmgg%d9#T_prFI&EcA3kqMGHlD;xJZC8B_M~On;gBLi zA+`BG4p|J)^$z930g+DpoL=*tL7f*6=Z;g%i37$t)o(jfqSf7hd>>nNY)@HrtWx$e z%@6;=1<-=o_ZjT3d8Fjt?|za?lMmnP-Opur-}K1+x7lv~!M7&QX{()|USapy zj`JOlKXmJ|d#=0Z_A7kv>&XYd{^0GGF1_ya_uu;ErPp0>@E#i;vG3-K4%m9({#zZj z?`B8tv+2TpH$CQnEsi^Q>*MEb^U8&DPdQ?@6XxvzPB~(i(-!P@$`L!Acy?#>XDG>2U{de%ky^PC0y|V-H^MsD0Nxa{mo&RUf$9`a5j- z!p+wIgWrG7q$>Pf+sr;p%1eqheDHu0_Yp5JZ?6Vs;j#DTc23?hnK zm|L)$Z#OJ#4y^FbDvsyf27c<+MTiWCr?Hh>o%v$`i)dK z$NZs?S7d7;^kd5?qC-3!Km?F{aN@_zZ9E2@w-Eko>mn{R42WrP~uNXnuo(Nq{;-a>{l-<7TTETm2;=Im8V{g0nA<4 z3zqWq(;TkF#)OfQ9C-BMIl+J`#F3Vwqo)v9Ne~5KPKwW7ct7%pL%cfS)aEMp8YxB_ zVuJVw5YG@goFP-r3^%n5K{Kjs=cHQ_E7_&^n`QeOoJ2= zyRu;r#g!-M5Qm8IXa(4)!6$yMZRtynDbN%^_MjYBD|SG~*!){WyDL6=LnUz4QK@3elN^{emtFMpX^m^o={ zm2b@!4qz+uk_^hfpc(>g2#Z1{WfnCqBC3nIV2#consQCH!B78Wl&7yttHOXGkaLuB z-Zaz7e{_>m7yK7L{gD^A)_v)VPCV`yHG-ubqJhHzX=ai_fj4%kO&f}ZrLHs^Zp%w6 zSnaR;!u6Lw(B(k^H3(cUHfv73R+~(~S%jlEo%-!X>(IngAp4IPqh#RSb0&|sN7fWkCWI3SAAG+q0p*Gy=N z@@=p|jMM-Llu3cR6jW%G@G=7jr6_;KA%_hI*?RF;1(e=ua0<;UJ~WFM9TTkCV!}iz zMPlThK=2b32oObA4_)YbW1cSNCyRWCrQMu#2*;n2M5qPmY;kXmP`ttslt>*Gpm-6Z z7cYw8GkF9KkvhFZ3ST}EM^1*4k)PP`0i|8q8ue9G^3-?BS35ryVV;xt|?MPQ(HvA~~d(rY8p+ zmcU#ZL?WdKXE|gSIK)Y6q_KpL>_LhGTOtl2ln9|=N;%HJVNgUvkQ7{3%4eD3+!K+R zu(o>HNi=&}b+wsNyZ|_XzxbY&o=b;YTpodPzniF$d_ej9<458LphLp_YW-opLXTXfJSww~wjx5eC@*I!$` zY!g{gsSxvcu!>JDY;VzGRmEpdwk>8gsX9@s=2g&XNA2u}u1iaa8kUrz*zU7Z5gmd; zt-H38R+X);RJ8gpCvW(A?biyyx2-$~t#nnR;cpLDZCKf>maGtlu!646rOhFQm=Wo( zUd{pdD73Mum$lr0Fm0jY8FCf6T2&pYP_<(5*BiF3Y!aztt$uwNO3kRyV|utjL&2)n zrNW_>AyuuGY4K|H3X@BEMMQ*kU21BDGvCy|-kcSJr@Qq^GAVAn`Hg3*+-*8YzAk#~ zU;B9h1JC7LD?jI)b^0||e)+nq{YUn@)x~y}FZ}FBFS@`VDf-K4&%>XqWVX4iURErB z?$aMuFME0U>{l;-$6L@ z7_wA#Uq-in(02Rl#H7SN3yPb&M`Tss+~~$fmM3I+)s1y%qmv$hflI9hJIq@|!0nW{ zfu;cuN>zz)EN+E>n3dux%rp!oq(Fz|M_XDr&jC0+Tm=w^<%bB)Oyq;Zx)Q8cMD)u^ zJ|vC}(2#g)Ia~ruf|iB>l#mVJDId`hBt9Dy&h!_Z7ITX(o;XZNwuT-EXDF{1F6yLo zVFnO@hkj9{x#ScM{jS?4zhc2Jc%I+z1DBH7!-Ou>G;kF_(mZlYX8{%+I{ik?u$CG6 z1G3q|!<;s+5OoDH=w*e%Rp{~iMw5%DYlVM`=~+kom`6U#1ImDrne3u-(%cr2F8-5k zrm<*^mvz1A#DL!Dax&jA!X?yB9mySclATUS&jD=`Kj zT41R3Iyp5!+?1tp4V=p7~@nZ*#Z_#wFHA&T~G>NHU&8 zP`F(Zcph^hAgYd)!__QG74?f^`4RiM#@%}#enfJCQhiU0Wo^q0;mj_XEF@4`y~sw9 z%8Vt1>qBb;J_L+{+-SBKLH=+!$Tw~ex~N^mJV@ZGVi5iM=U(8J=L+cuRbi@#z5MQm zXrv6ZK{PaeesN>+`>kG@5m(7rKGb4xrN!fQ021rwl7!+T?fEq!v=!l>tu|tTZApLNy^&!&`Lu}6u(f)O# z|Fa+cwUPvLZ$a`+uP*$UneZdb_#I{x%j#mA%XX97R&MvX2OBETB-tfM!W{ycln{s! zkth?a{227vB;=||C`%kzBM_tkG&d$1rVo&$5OF{hBWQl|$AC6-47#gCxHkSVpu!h8 zPbRTX&k+>EhuA=9;Xvl)SxJ&e8Pi*yTab$d9RoH>w5-bpw`D_H6gg?~@EI|Xr6Mgr zf-aJvNt?sPSew+q$@Uus6Jp1oEeOr5<7Bs3R&UK2fiyFP;1EyIq0bHtI*NF;B9#+T z7%GL}DbWjtA-y9-5QojuteAo2)^zQ8WA8KGa>I3g)if^Yq>YuVlQoB%y##e>DaqmC zEoQd-M$`mzNJKBr;SW`$1Ogb+1x5&|DjUQBibFsHh&Z~2>3CLe*~$teDTkSctPVn3 zonNpyJX|es3dPGP5qT?h4oypXz^u37lM-_JNim+IU~ZTOJvP09o#mvB(bC14QZegl zo)5=HDoRn=8$?L-{2#K2D28vi5!(k#0N}zSMD!6eViA-3DTK3dvXnY z6wAut|N4Euf%|dwKF?kIz?Vwdp<)?et6u*06U)`hKltVfh4XixTK=6Um-#rh{blvC z2eY4ecqw@5Q6K)Al+2GUzr~ln9=h{#^)mS8!#6y9*JbKipk4;{mly3nDTGzZix1k$ z7V`=7c3OPkc1Q2G6;LuCd%&Dy_TTbo{Bt)_Rx6elA7m$bb0xD~<|FrBfAIk?JMO@B zj@s{~3-(@T;ei`_<@xACwmo?7E!I{q+sahoDk0Tk3O%0+@jYkNoI24BS3zf^Q^}@Y za|;I5gD6VoD#yxabu=k-JScFQ711Fe-3G5kZKeiJAIH^)^ zvQ7}LGF5W&2ZCA6t4oQh^rleFs|y28huRPt5VasTIHodH9b2e)_{}$~{l&9G*IQ~; ztSWH}7^{vnb4I;PmR$fzA-Y4Aul0PjIhO>^rM!|xbmp+(;T}3eT1C3`k0cAzZxKn^ z#4ldvl-NZry@(v<6+MzkrvR0}s$-kTpZ%0S#rAgixt_be>T;?0sCv0dSqp4842slN?qkx1&_B zS`=&zNU|gnJ`V8PbbFgxe5@1czgE z@;S%!mH};?KK3OE0(UA2*;~wZ*>vGA;Y?&Vg^3Tx1`%Zy|4Hf9(#AK+LF=oci(!_h zNDFpohA3*R#B5SQs)kHXLnE$?vYza4EG3!NQld*l=w*qlz$J$>E#!33mQKt$He%O{ z*pNBJ#8SdB5YelQDp;*UoLX!=chhw99jhRBcI#Ms8ScN)Tc@=jG}+*o?k}sCZ79Q4 z#G_tD@l>{FvzI?~AKd1$UJm4TQ%frlPzWRW5WXRD43eLixr(S)OG=gsvw4nKy{uAJ zo>VWZkstPh!?ai)^)j}q<|-*GrPfpew~zn=6uYP`Fl4}?E(?h43P+$+f(!;r(A z86b)=+!0L)zy7%@8d;YYKTG;)RO@ylKmYghpKm+Z@1>r{?)7=sNw|91cOd`MbDrZu zYJxFW_^Y>O4iV09%xU6e@!G7fG?+iSnq4-pImS9-2`}5td{Izan6q=NShNC3KNA5Z zYXegYPqGBz+ygz>+@j8!vqz5Q*;1`FVG~a~7JeSd#%VZgF~_EAFgaT^8plSnp|oT* znk|MQlr*M4{oB9Nm0E@%0OI@MiF@8yRsN5^`h|x!Q7kFd5zJ8wI!)x!Gz*l*RmZ)b zI9n`xX1iyz{gNY}k|uCTmi9J(5#{Vg|bbL zaiqTiayTFg6Ohbt2~KL9k7VmYZ&oO2dD5i-k`EG6{kCYX7n;&Es%KT(y14~8Es;$ z-tu1{Ja1At!~<_E#5G%VbckP=<|nDLgpNNwhyY$@sDcl>aFlqo;a!~VFZ%;2>md*D znUEzpRrKeW)+wXy>>w`N@#HY6Y<23vbASk79mBxMGEE#CJXLu$>vIZ+AZGxzY1TX2 zmGafw+|B{G(~y!5iLVa2c#>~8g@Xq%yLHv(xj=FQ&jip_ln_eQkCFDbhPkHnj^qDW4%eI)kzwGccYd*6BCL30k-XUnzK%W6+s zBx*F3n)|i8vqEy-LHl~b5M6<$WUDl;X0&ap)>9IyqoX@i_hGXiS|MpmNUeviRRcnl zjUMx6MGZwt?W3wyBddT^kt$Fnr%DY@4-u5gMhX?PcB@sLM9{4qFwl$wp#+5#&tENreoHLQr4+;-@{2 zt!lpX5*fI#()ik|F0q@eUREG0lU38fq-y@ypS|S`r@rR&g?5?kFMA%_zfE}_`{D~e za``2nx#nvZU47~2ufOt=K8|gRx%Y(?Dcz5(~4iw3wP(m;HINj zHn`1aOGHFGR*ao%DWN$*`U%qDu1ydg(8Dewu*UN=QRZd=ksg2}JcM6b&Le<#iDu9>g$OJam>NBFq7tgJB5PHI_Zm zS>)~Xx;nY)W|V9i95U^&v5TmsO6|#R&zFkMpRHIpIb{lg+XS`T8-b*UIWXAx6OJv- z*hpnjOF7^n&asF>#~g5G_AnwP0TG1>qThwEx!q(a$MEPXV~}x(6VYMLmt+*$I3u)C zFO$L}6)2{ewlgISY0i_B3naCYFe!(p3hAsj{ydDK+iq(Kt@Ty)a(CEnwU!fB!U`=l zg<9BFvRe3wCvEI1A3T8i1YGTGb6K(cz=L;d;fQvh8>6h}m|z8%4;*W}AVrFYlSPMT zu<0cwcH*EUOo&EPCa2HA+GTcFy=?CfI0S7jS27F4BGZyyP-DGh%n*faIiXc7`(*${ za`=xWs3Ej)CCz7>+zI76BRsvVPs_sTJesX&F~D_Zq(hx=r{qDX-^4`mmL_ zsPz!F-&bGu;urW9rDqt-|HhHp)qxc!3s+sh{ou(W9~g4xLePm+6bPYIGl9*7;kMq4 zjsP14JdA=4Aq){2qgL^1YYY3_Kl?A6Y_RS|>#c*$WWYemG-!c3 zsRyUeDm1T}W3{7v_6;A;W^2t!6VDLHmB$7QUsI)4!qE{&r6z}Pc2|Mi7JL5G%Q1(D zG*Irh6hld&v{PcR8X&d?M8RPA@}bd;PGprK#x|KnDm7wp5z9^4CDj5f?}GhE~nvooAVf&%oqqdt;}!~_$=TL=qxRM znxKku8k`Yui7G}2ZTQi-x<=sb@Vw;D1~nOom2dKzU2uTE`7B-{rH-BiyZGQiC(>*$ zGmSVL4}P+Vq^+~Vy8|xNddX6PsJ8*iJ#>iSYXo&+qX#j72LuhAcJx9h%tNg%kgL23 zax%4PPFvPFR=l&!t2i(`nML}M3MZlv9BOH%aE*X1yLwG0wIqJH78nI?I2>%^N>6iD zas;TI4Tr*9iWANsffOtvooOCw5m7i$j2-5{;M6s%fvm%!b{HMuWT-S|ktzYQqAP8Q zr#92n4#7-29Da`8kbI^P63?iYI{}@85NA;5i1Ve(#W-<#dAY}})yod;CO@+LF7GdU zky**CM%F5ptDP0g9>xaWdVHCOvA?lm@@G=^mwoRGOgESP1@q)#?ECM$`i`3~z4iLf zE0!O)?W_0S`jw}be|^QhSE`p!Sg^|>yKOLU_YDu*V}m31+NfH%s`<19dz?CdcW*AA zc<7GPhq1Ri{*Y}?n75r$T1|cQ{%Yh&rM%>jElxaa>ywYzMzL&*dC>vu*k4|--v+)3 zcH{wD{g(Y@_h?m{l2Zle3n3o;0zNPTeDlgH%*tCE&}u*>p^{UPs3e4=tLCOVo*!4u z;jsWv;;Alu1)4UslIM!?D@B!oL@alejoy{E*{Wt#{n3C9AUM^DmACj+TYzIKQ%^_h zVv#w55VAPgQez^bz%e7@q$&zMD4fAY8zP>1MGgGI&u2gN+wHZ7Ym*%h6WI7q6(naI z$`$`rxFSN=%P~2jnLmC6kfm}(gI!~G@rxQWCl$rM@VWTtBT;mz^vQ}{m+<8f!cT@A zvZbXKXFmcImx}nSU;f+`Uqd`!;jDZfZ!RmMy~vC~?W|tb+I_y@{Bym(?0M{Wz3t5D zpGlqT&!n#S%4ffH;YZcWzV~(IS1!2u+OI9Y=Tkz}x zj7TKVm8+u{5@pa)W+8Ec`5>r_YO$e@q>Z@n7!HS=VV59IcmSWZ2(LFojW3(DQ?gV3WQ)oKwIu*PV11iOqX+q=(ffj`hF2@R)g3mQ4#2ht>{L} zB0+g@c?S@Oh%Jz?Wm;P}{Amu4s8KLSWP&(=07eIlF1>scqseIku;r^tD2gB}*de}( zphG4Agjbv)2%$$yc{hdy$rXTykhz=w*6%VwH8Iatg9Td zJEG!KUr`2H5 z!R5h%!Lr1v!}3Y6c;K8FBMxD`SaM^w9i03GBk+dCp^K^!G_1zj1N)_w*eXdQ^I*K*Es z-3Bq-?-)LO^GF=`|MZ}B~G|CPk=&c?JTE|+4+oZE}VB)|1)32|)>hdEG zpLgKCa}Pae|9J=Qcl@!7DiHJ%w1OpKda%+H3kqQ~YWybSaJF9Vn_$(;aw~fV)QmOy zPF@L-oH3NLjnddnhG{x8*=<3q%?Hj~_wvLcfH*`n7L+^F8!s9FZ6&-B9OH-x&tkJQ zyA4g_nBaem^7PSS<5jkEfRh}9Yx!V+^WQ@h%(Rf1`a~dD2%z3S6bPY@G3W$tU z%!<$AY^8*tc{u9jc!o>q$uT_q@q>defVn8^D*s~$NEJmPz-h}Rg7r#ouIh?NIm$2u zr#6=+^L*uOfiykvC#yxwH*Fd+J&1#jIjK^bGkPK6!+~V^kgL&0Fa5buo98riUN~i( z8qN!coraww<3a&(%1pbgJ-lVZ91dlfp{3ZSC|#f zie)9UZ(@5SyL$Pt<+s^l_87J|mwoK(wi_bg%Zf5}qC(H3U)~n<*A?#0Rn1--biY>6 z*_Kt!DI}GX4ppXZ@ybjTB_14|9{Z$T{#D|sh*gNHJVmHpo3J$aPhmfj`@vkjslX(N z&0){kl4@I4d8oq002He9paABSqUun+Y*AZ1n==*5m8H7UU!9(ax>c_(J`0M&wtcC! z5lnL~k>Unb5S1)Yy{cO?gow={2j~RcKoLkuK&Kgl0-xu8YETh76h2#=@rLrQJ8g@i z%4QV6webs|=`2F{#v;=UWkTksru|UhXN1pI7pt9tPh_i(J*Vy4UyzS}=sk9pmCW8{ z{@f?uq%^2m)XszwYoct zv&QYo04MR-HF_w=|q80g@TPahwfhlTQ)dM zLv+rNa`$xpVoQ(|JWeeW-3JFHbNPdFI56CUV9o~&5QYFE6CALB6hg71+t+q$#wl<- zF=I;uPCk?$B7lcQgeQb-hVlV_hqOg0YB*UM98x&gT$;p)nCUVgCpoMOCufL98G0P* zl>ypPn;|VF1BM=)_(v@0cw|EhFABY@0Mh{;n(_0~FAQ4AamYPgB?sb=A;3?n@*koL zKr-hi7e6lPtsq4&z+nr5SnMH~hc>+hPKsB_YAJ656C?%11t$n6l^fdh*E?cI96Xk7 zt+fo0;ZU}C>=tJl5+6F5*2N01AbApzOJiI@(1DDVOc?+prMgB$^i#+Ir^&37=pz;M z$HvL5$7wCpGOU%A#g*lgO(Kh>RlrgW*hjW&Y)@Ie3?O5FS(mC=+sD2X%M;F(^;ZdJ zRAx9+bR*A@D1u5PPDEXK9#3vZ7l`LzJs+zmRU8F zK?8@h#2pMwv)GUcAz&9h$e+=!P9GE(km_Y)YLeJu_Dq5WiHp2q8JO+;hY@q=z+c0A z;>%~Qb)7c!u*~}RUZ#dA43nvZQOhC+VT>#zu z0T73})&|q1)MOtMBuoEfWT($ITclae!ObXbD@LoeR)Y||cr5lXSl=2HD^HI%SjKx8 z`|p18w~dsx5j1Jlzy2!Aq{SQ&LcED>L1=H9LWfjQ?p5f0_u3t&$GqokvFYA>?0Wao zTWR(RGebPFwzMeC!xo3Te&ts!wmP77Kpd`CE7z=6d zQX?Ck-G&33OiC0JC4}|aH7(B_$_$rUpF2b(SbAw|l<9G%L@fWaqkxA<6e9vp(0~p| z_L+9N&(5v@0Hm~xMZ3e6+`1$ay|Lq-F2oKYK(FZN5vD`_^Ms*hRC5V#=2s3kQ8^}u zdL4EVAc9Bs{OII7GRccVID(?HWMYG-kOqhKLcpxK|0vH_)_plVT2XLbfRZmRhq~a6 zS0jQrKu+-M6)Y(t64D3`oe=zpm}$U2D2S;t`GFZr!_cM+b3m|jK`*F_o@`~Eew5r! zEgXf0&IqRu3M+^vypsrpHVo)mR*KPKfOCzYHxnEfQLHW%&+08(sg*PibS_{+Un5}7 zJtmrmF%zVr@hZ1z*6Sw-xIeb;bJ;S05`Ua@V$1)o6_jUXB*CB%%vMqbj>rx4LWZDF zlDIa?kVP5h2#%x;gq{)O2!vFE8KrYTc9SAFs|SxmR&r@|S1~xPT&$dNoi)x26nI2u zlT)k<2%af%QU!ZYS=Fp4RyC`O9eR0Lu?&>YRnr)7dLG;6vZ~qh*y`mc9=gjvO!)&T z&tpR#zW=6&?z>U3JpIzwwbL!;i4h z&r8iVf>m=WHHDh+jFW?o$zaz}q8d z)cVqwiyIBLU$yp3>EQzmLFjVTI z`zgTgDsz5DV2BbFUlQ}6GN0AjIJLSs38924KCsINExHg`5xzqR$8Ng*@+xK9%0TU` zUREjFT(;w^Ubd(Ff%m;b>%nZbbMG&E9{bPU`kGIF?A=P{&wcWJUS9tA2j6-1*DmzE zFYhn+^78$6+<41%ms!YJcg@~++=&dCdy^Zzwkp`k;*R66wN`g43=V<&nnFp7o)nx; zB2>BMb*n;wM|Vm(88LTHBnaV2XRg~ETcG=08sdR>f7F!&T0}qtJpB$+K0J&~o1n#v zMJD$!Sk+kEij)Sfl65E|{#TN4%we+JAi~f5ry(WiDA@EefdN1qri$h=zzSj@#*|Y+ zKP3f1mURk=AnGC&WT-C87>bAns!-(CY5|>H#72!QC3=Yi*5Q<}!Z8$V@<0lkUM;+V z!^VWe0V&Zk#2h6tXfcx-E>X)63N|pSmNs1gVx!GQts=rta7t!uF3uiWAkK3{DHE6hH~HZDLF}?fvPnE2G=N8Z@sL$OmP*zQ^v2gu56Y)JY+66 z^ir5J{B&})B9hXA#mp0+x2R1UNC3z7TOe&h0GPE6{aE|8q9>De*;CtWoj^BIsv+#WV z>MORNv!%n|{QckA17^{FQ0rt?%nHA;(9juSYBinArBN)KehTE-SD01J{fr~YQqgco zsbrecMnM|o7Gl(#4(QJ$b2=vr-}03kT&>dQznm0!D`6{MQqE{ls*N%z;V83nG6w!Y z@NlWC7B|e9CIGLJ&6&v@nEs$4Z;}A#gXAa{xPb^DGQ-t;!@w($xA`dwhXAMHFctu! zmvvTn!lf~1_`p3~lG7T%jc3WJ5T@&<*P9`bje>@O^DMxR53lk?u z6+-K>YHfw-l9ul;cSWwb5z7Mg&8>wV5k* z7)n~x8>d4+YUm+FBnk!&IF$ccbC@0K#pYblVl!mQMnnVc9N{*4wbh871MmTPSW7UN zHN@>ELk>I4G@j_Ix0ED>r|2N&8{!&4bfu+4VTK6DEN!B8B4?U)bGTC1#nh834)ZNr zV+e2((IqGNAv8+}_aJ)LMrTw|xj|cAQ3U}skmXWm#88+mf(+#k3c$=FwqoQY!Px4e zQx_Og=&UD|et2$ENLDzU=Vv~|jDg_9jg_ZiXN@yrP@GE67bhC%JaUSukyXJyi>*dh z7u#Y6s%Atx%RPy$OV!-7*>>gpnN&|@+gyJ10spXk!+m#LXO~&kyzHLq@4Mrg2kyG& z;rnm!NVb2Ky6%eeRm%QM3i#Mp`^&a~k3Ve3#fNNXf7$!X9<)C3uF z+Ur$|_I}l(y_C$o#cXr=#zs zo;^kf*nGhDdCz^eGSFr&wf=Tl4d{zrd+xTQJH7|J{Z5X-JG>YafH>0zAr2Ikx>Spn z5ZWqJg(?OX*~P#Uvh9g0idCuH(@R{@s{B-YVuL8dIUow!u(`A%Dqt(IwfzWyhyLnc zL`qmB0#Us(iip7>Rc#q#U?nHf>ERNi>TYzu9uU-vh?C}guApX)tGf71VVoG~p+Sp* z6jx<6C+SDgtAbV(E0BHNOF7)b*q*`mF0ZzEU~5fwm}hL$`Qy z&+6vu7SmnH`O+Py#hW`mWOi{&q*XCa_bGR=M4WUu-3O^CYlme$jm%}u}c|sdb&R_}p9hi0N?{5X2Ak&T5`Dmo`2F?s18fXf2Se`m7vk-F&;fx;v{P zTiLqvcGp%8E0b3|wp?{wv8*UY{QeKVWoOyb+MdLAXzSS_5HjExTPz9zlnsK(QErp3mmSkUKX-=B>_NM zX=pjmJ+@?TcqM^GT+1;|25=PorI&}GCQ{Pa`4b=cV3jFm3MsLG6 zdmX^H+4bT<*P0JP>SC2BYk1-{?$6cKWYaOaqAm>~et5EyDRQabx8+ zt|SMP2T~1;AUZ3C-bKtI+@#7nPLy~sH*l=u2@p+77u){!*Ut!YtCa_yNPZrU#Wo}g z2EBlXoFDYD$R8p_*CgT90M6u(>1y(*=enL_F#ULLGY-*BZAmaNJ-j| zEd*U7Myf_;u)&?qsf~GvkV+>JNMk_{izsUZU7%S_IaNs^G89FmrA{w;ffL~d2A*gE zLj%fQZfmmw!9c&X;KvhN0qO;CL~Q(DJ-Va{WQb>aFqedJ5pdOVqB(J#7o8(c7N<<7 z3AR9uY~xr(4AjV#%zgN)?PkRGnw8HJp6QpqdWG5dz9ygidg6ZY!|y$&UcPtf)%V|d zt@oF|`?SwxFZI1I?=O28`?hN?xcSP@eE*5tuK42n?Jpm9_>RXMG%HR0&0crX&S#&r$8m>kdEhQDJ81U}79FxiFF$+o z|I5$%o!iKvuSlv+E; z>QkoGiHc1wag2V~Mu(Nv1oh%~NCO8T%1Q3w1a#8i5V4w+2|MPPIc#Nufm9`Gl`lFI z=|`tTjIPFv27I%NO%FO|JbE4SmcnXk7STZn=K=i$5$T5rkV1zX=tlrb9Ad-3VK@pL zg&vX8A1I3LDOVkPD%%!wJInCiX!azw=dqQ{TJYu1f9Sk-zu|Rf9P4{upZe&#KJlS< zeCW^L>S1j4vR9aW?CX~6F1_QX$riI>`GGrcQZKvvw6xQ@kGDGQ&f-SXx)Bn%AGIQK z2yvgq>F(1?7;{8)0H@GO4`3h?v(s>hcW%avh|N-M=+T=hM7`m99mYl#9+X7V#x8_c z08S@2asV4WK$r@pQMbbJSWntCloLxD6u|Sb6wFYjQ$m|wnrQKn~DmE#yz%Jn;fNr(wzGvsxBT z7-WWNl?BI)4q0|7M{LL}*R8B5Ng)O%XdsKzAt^o(kq3wH0o(?BYxsmF3KR^DQJi`m z61N1Ulj(*-+preSY|9WRnLMcrVo*vxZ4P78!pj&s=G=e-R#5O10!o~z$AeO&yb`HHbPRBa zAvQoD3LaMyms#;Pnf+6Pe@u4SH%n=2ZjkjmcHjH*rn5TP)-EJPfY*x z>tEn$IynVUoF-(QK^|N6W>D1={bn!Dn0eLgN)>!|hBy=&9&HHm;$_!`aZE@fh{A0Q z1LrM#xN1dbQu&oelBM0D1)Je8D6*}qp+U5M(`$54%u;~g8V{n>wd0Jz3eb2d(&|C4a{0r&yEx!GC&=*57CP5V!3%B9^H{K$WtGv|V&) z=81Y4JZSjb2>jglS3mgK&wupuzx(lzfAZHq`te`=;u2a5S_pqS)WtmjafVKo%F`DNjY~sDx%_8{X=!iN#)xFy zA!HU$s^LQ?$U}3?71mD;-XHKaX1HPEq1MmU zX3ika^5b-f7z~#Zs@0h;lQDK~3T7r{h?49OO$$;_Oo<4C2%hdF~eqdBQJS>gcBTT_Co4(SJJ7>-eu=@cfOCovDsS%i|b zWoV*&dV$h9_0-Ftlc+PLa|#8luf74mLGZG#(S4udH%aC%AN#uK6K_9Y z*Y#`xS1;RKK77v&58Z9u!}nOxW&h`JxzW1eAK6U=Cc9~C|zw?5oi+0}_QZIx-dJOiwzR@*8zE9($-0o5G-4(ai}vvRlsu!{A>B}Xqh@-Vd_&UT;e zFS|F{!lp9~YR2}dGpDvyQQ}waD&*nnM|E*K-6(D`j4B@S(9ES#y7EMgiKoh!LWe+U ztc#SO)XO|%#rAHo(_dwZU$M+0=L-!p1VdG`F4?dtoCDZ6z+8u1Qrikgfs7)kJk(Yf zvx`|Q%n8Tp1)V3P@bKAT=4j?Hw>iMc0?AKrC98`)fURV<(fq~FdszMbwwvuWBYFVa z-%HtC1`tnWU-+r_zWeR3^*r{QUw?}4eSPx7@BGMlZ~x-wKJ2M%U&+4VsxNzm`L^q? zP%?WS+b6%=yVZ;>?cAZc|rFqhpqh}b)na##nzcMm}8@=UbiH-r*TJaX@_T) z0naeE3VpTf zB3UjK0{!p;v;d|gI<+j~)Vi?Xfy*RF98p(kgfK@x4VJI+4=izQ0C7?;V3+3flrcCf zAdBTQhw(Fn0pweTa)8gdfv8t2eAmW2B;rt_03;YNc-qOHgE{y?-Epu@65-a(D&khlTk@kAE zEr7dSwu-Qxad+;P4e7A;$z-82Es$p)x}JOl`{@;)%C274I&2SG?aZS5ONddE2XSdp zMvO)6uwVr}S*ud^gtjj60&9;Em#BeC z%Fd%f1yyqvQHT;)4ojVkab7I5zQOTC=NQ~eC(q6 zZk_%Dpg$&S(ACbK&T_kSBDf$B0VV^5lmJ$kkO%3dN9Bd+jPOQ)+0Mx<{`>#;zkE7O z`K;C3yp?vUu;C#!(#sh-$?_jQLMu9p>SfhtnOc?AIs0Su(bO10!m ztn`{))^(=VSh!jcg2^%+|vu5VNe$JtP6Hl_kk{`LdRpl z1djpjjIhgGy=>GZq2guuHj9+@*~L63L4!3oXV>Rk;=t~J7O}~|B3Ch>)C)(XB+Ug<8>P1S zrcKZ=;UQ%wG{?}v5E=X^4B>ayaF0|4K4dT_8yn9946{n&(fSoDH?-6`2fG?Oiz$@z z(HjGiS^FC+4r&4%O1D3 z#q7asFE6W?mC5Sm6XxyUk?a!>-TuhAo9MO0tX`ge@@wOh4xjU?qj!GwvAdpg!Va%L zaYy^hXB@kudik)uHt;>MeLsuec01}nY97I-FDv2jS0BKsZ3Rl zcq%8AjS55Mp)wPZ2^K8?;Pfk0;Y#BQQoUJMmhvBwQ3wqva8F`mR$v19F{~^$KU@VW zG(1d@iW3iSYw~HN)*JfAG9_0v`c` z``(u)vHL$M4`W|&{-66lDPP7`FZ)WikA10^eII-2jaMzZ`%G@a3JKSE~ z$K7AtO0)rr-q!7{g<^9L0(u?dblY`Hf=7o?h%)Y9bYiYpP$&_J0Uh9UhzJ3*#LQ)) zQ@C?Hyxz=_qJ+098vM)+Co`wb7JjQfw_ToC#0DtIT2)rFXlbC+wSrMG=A@Phtt$*b zXDbiklqZD&;tB>tZ;zQ>Mt#G0D^}?U3d~iYzX*I8z@+8jSedV5C`yzAXR#E zo}fV~$*F?t#l|{*3PA%=0tpHqqzZzKOWOG9XHk|63TA9P$ADf2u;rc!fB|#mK)v#? z@7C|>%aKiq8N^;}haRKYVaE=pP#lmGtT{*h*v5gxLxRS0;-)zZiU(na2x#RMKj zw4flK=r*_MuRtg?JZurfhQbXkaS6x9e=QOIgW}ayD#XHbz%--oBM`~#*A;psO^hYB z7%8ER6?Opxg$X{SD$Q_$I2%d~X=AY&f~~t%ob+H0(!hc;;d0E&)@~CIvAPk)ab-Fow|+*1Vip|CMOZiL!1C72GIf03192PH+5A> zCS>#0!(Hybrj{Rn(q>}lI)3p&XMtYU^_DKU5oC)M%oH-gALTG}^7%jh-+%Vf(+mFK zxm#?yfjheg$|!l?x$pcx|Lfm4p!D`~v;@NGY#NVwLRD^$(4rJLVu;xe=LtVC@Wdf! zI7Ss7xL#e2R2iGJ(?y-NACP3-3)9wXA~5<*bL46ci-VqF>8IUg43F} zmV89?nz2n|W10y=oLY6CHK7_=7qzyGtzF;rhO@WdW}D46T3>!X_M!JvYqd{_!r2d( zkYE4u=kOMjTD#2Z;+t;%x|!zZ7x>Ai9KX}f+a0y=h+q8jrz&L=OxqLMdM7W}Pk*nI zoF;Q)C-c%S`Bq#O6*ijXtekFkCaj#NIPHxxD7Q%x*RsC z_`nL69L5Y8!Ix+J~<|SF$ZQ{c_WwdT!ZI+T?obGF&N1yX z8J=DV#lrJKHWsQ#m7UhyYg{lFT|ZOO*A#y0&#oD1dL3^%KZ$Ddv1Op?LO zTxn;KOL&?&h=3seoYVyw;854>V8QW|%55ev7b9k%DAS8R{2y*Ozsw5!PAhE;^aqL0 zwxcTtMvG^NP$E+2yK4em7iR*ImVe2?!?be*KpY}c$c?H>xVX87mR#3SC zSsP$ZRYB3|5K-#D$kO0p9gaDN@v~cRvV)V)y7-0}kS!O+ zUaSg8LnP;44+t=+?HK)2%3ww@Qt3W>ItTU=))r`tFtujza3n0p5pwLr(0((H7 zA5(8)a~II%bI|Has^GR$@oc`yvc5J%J!tSH6? z)WY-#htaz9LJE^|I6a)9N^Ut1S1d~lp6R3MFHr(Wf(Pxm#{@baesT=LE+u|XU;;&| z1q;UpQ55$)w!*m=met61oR!RhI@%vG>-8eD{pBxw_PqDL^K8#!`&TK&vXc2zAAQf) zzVs>2WBcCMv{?S?jaN@zVYa#KNeQbsL*F-J#9TuW!`e+s519Af$g*c;D zs+O0OJNI=Fq>7#ZgPX7Qq}8mSiF%bA1@KU*qNXE4RrgkY<`{+aq}G*$MGAGXlEa*I zkpU20h(O@*9MA%OQj&Od42Y~lxL3W*4K6vP0W&B|WEpjs{?W!$OPdT(E_|*M0g%#; z2vEr4!AUa&Kysc@4A>;0Sj2^&ll16?b0f`KnlaOWx!ydaKc<2yAZfj`}rf~dTK?H*ssiNE{)6;N3@Zc}W*vdf6f)!dgo*c`>Y{uaE zS-KiB%*{a7*%dNXl*d^zwT)vr*A7*>XbW;)@5I~b9-n(9aHDVSVb#%1U)%D}?Y=To zu?)s8v(mWFeA!d>=5l|#j0jgduXt=SY5C7d-ilP7or4_Ui57iCDmi0@Ao4tj2QwI9 zLZT>76fY`bbuk1ejoV^wE3mE})ke4aGE`y_Q7bDM5F=s>Gi{4542XFm&yC|qZ%YWH zP?B>->(D$w&uvjlGF+Ebn3;{EYpm@>R~UL+aJ-;oV!M^7d38~O-=oxiaMtBlBZqF9 zW}0~=OY}%4DDuy)Hh=W;7C_J?gg;fviHvL%J1IPkl$;sti2s&^5klVXI=r zmHL+nEpdXo?X>-l+iv~5-}}El`oZ%o_3`jZS^Yo$^B_u|Mo{eG}G)a+gbKrvy$0_G!-R5x+KI|0A$-R`Ngl~ z`&rMr$npk5iS10(G6$IM1cW0x9H}dTV`j3_C`ffPP_`k612}_{2n0h?hQ_McP$C8m zrBSvoZ-?Cj@J6B9ExX*{43`XOW14UIIieUy;nYi3P|h;r&ZS`Z&op4#T7CL4s>u}M z8=S+sxGEyk60S>Dr4EirmFTo^fsn!sDP%lwy_BcHSztJTap3^{Jmj891t8(g0Tgpd zE2wG>tqCLiI15&O@B=Zz>l%nGtr76!(UrKFCgG-Eb!eIyssf6cCrkhcLCz?e_@9!| z#R1~z#PJMpSg`qn2U5IbUHz!VfM1&fkj^Gu{0wq~OJwN;IiSr1sp!LWQKWNd#s*3P zWcb4(XSl=!JchEG#h8aShs^1MusHllaMphwl8RsI9A;vu9ah$e%!*S2Xp2+VEFj8~ zmJf}c3^a4fOyXJ&>*9=FEqtU74ps?MZ&{6#Q6`F17Y*^Fv#xcBj(Lnl2;wuGD>sgv zEKVz@59reE>x2cnD3+DX#~rfG z;)CZ*H;(6cdHI!#cJn;;$%pT%Sbpts`<;CF&ft{`celmtQEiW8AHK)B^LBmNF$ZjN z#xc8`ed65LF4;q|{F-C7KJ)k;UUS^erysM^2}f-2|CfF5Yi;$iPe30vf1WRKxxL!H zam!XM`h2wQ*J?CHA6!|d7}SfXrmKuq7{;S2L~$=xfm-M)Cl!6KJncm99x4^#dKt1j zIDFm#>%8P8G$;vGi0W1~AhouStCY!d0Fhl3oJ?S#M4^hVC{{wUm}x#JBx!S4Y3$G* zu1Zt&jUTgKs&G;w5Nsl~bxRegWJv)c7{tqAr7?UIu&e{jxq%a25lt=SY)KI*%Hbj^ zaH>$aB+nbI|FRQ~TSWilXYGyGv()DRB~&p)igFr&keMJ0^1KYFk^3OFN3Z)(wtCsa z*a~E2vSJy7)}cR>vcLS5FMQ+!=f2VZN%j8n$3OUAK5*_^F8tK{KmX|uTyxpv<>jwm zaq;z6UgDpme7SDLgLjU4*&U>16{Pi^n~rnDeac4H1mzO8HE=e5ePa#fYUVxK%%&ncgv=MPR#W4ga)ec z6kvg4C@yTRw=u(MLx*67IOJ81#ZM40k)N~?&o|cPCuZT`$1|%T$A}XF>4}Z3L($=( zfu4M@+J~@$s273(Qnn%V7m?N>$ZbfRt#GuSDRo9{H)oF3KJaz@Ia_Yx^G818Z@U@A zR|IA0fBxJ5LscatuS%&mogp-9t+L7_6Ao#%4CB0kCdkipoDkh?&alpNUHaPv<(1k~sx}<_OxGqraE!c=)kX61FxZXHv7|e;|F=YHS z{j9XjSmI*IDEF`d2$1RlcxcNmk$5O4s0-V~nU#w=H*}+DWumpH(AI_S7GJTf*PgNu ze5sf1EVqwrLs`iT6wAuwDrm)W|H8}=uef27K%D795IC<8`GA2-IgiLs&|xjNBNjrz z4x;*{SD0<*RV*u$l`|g6hA3DnqSZ3Cn^7!8l}nRArVCn;LL90Ebk$q=rWQranY`sN z;B9~yI7Ba{1tr5~lnI~gOq6+uNr=bk<07Vudraduf_RD)-kijsw=9)FGmY#-?aDVm zTfAAMwU#pqrrelj#$~K96}g5Cn}Yg|TW?Y_dj|U-fAtIh9(C=Nm(lPqzxkCFpz%r- zX0?VY52umEU`J$Lhm%j>=D#UJ?d_h4>iYq1L6c+OcT zEIwMP-cpl$tlW9mZMKpX#lQZ|FAq3ypN%$N&j(>t&_Dgz->Q+#w`Ls%(^5?>|B~a% zVCaoF*Wj)&e&6UB18V?X*o-D%m$$85En-{8=782pSX$(;c`IoW4w`$CY((T4NV`#J z92--^#}lnCEl%bP88D_UZVqXPLXZ@%gn#38SNnvx%(uVXxa1P|#(KNt=Q+H?eCBri zdeI&7+-N$#%oF}F#HAeOK(mEcoJ2SM_=6aOsm@h&9*PLK$m^O-MN!o1!kqqiP}nsV zPJ!NjGAMk&Kb!xm7@kXbEw^PJUYA(#!!ZDdaJ_g;GZUuD1qCvafNgaklDIZou0`fB zdMRak^7F#VSN@H{fUIB}Ug;tT@MkwA&8RW25ZMC5AIxM)A<|}WiwFhiDg%(zY7=Rq z!efKv&(I9XhuT4pEzc<_4~S_-htuiM=~)KK4^Oxu|J9YvLuY8kCjKmH;Y29cm7&z; zgDybaVX+U{TwP;JB>hy;oI;#wtIHt-g~H|uN{M1f7os+v1TmygAOXi__3I4~!H7B{ z@t`gm&{6V13nwx%R&PG&nkbn*Dbyte?i>VND7w5jB>~(iiLFZ6)i)ULGNbqMvKraW zvSQicWQ+OH`=?K0-}lrb_dWUW-N0wQfMQwU{Kx}0KXBJ|_uX+7@Mlu0=9{m(z{kEG zy7O{X^Sw7;{M8H2wYlt*UmnI*FZ(koaQwV&`b()-AG_!4PnzfZ*k>-@SH0{nrR*>J zIQF7_H$Qd3PD*B*%ZKi^&fXlPTuqNr|);xN&6i? zf9Ky)FMGP!OM>pEKEvgMv?@fK%l2pO;3(NtaCVtbIdQRDIUJ>e(Nb2$s*Y3NDeJU~ z&9h$f3Ji)+RWr&iJ8t7fuUF-$_$A_{T|~c`I|M4hc2w!aR>jJ+N4VRTRb!I!gE@tG zC|5Hos7X~X>mBvAr2^*@RFIB^OA_PR@7=|sX zn5dW~#UDHX4s;QhB6M!E!dtQ!DDf{^J8Zk96_E{h8eFA4k8SVXhQ35`4^c96Af0eZ z0N->mAp~q{Wc6~N`LdzhKC=B~yU7Y?MfBu9q(0`azC2Dp`Rdn~F8tv8-u3#kUUke{ z-f*gymp}Ucx4r*8Z}u>@SD4ky-e11zs~28(`IqindV^xw7PH^FE!bMdyOFsWb$fL; znVucbJYHy(h2oUyta0b+zUJ1}y(z)&gL(s9l*Fk;#GoqqE%h>mLuZ2gX)(t1ICH7$ zju{UThi-p(bQJ<3b7V!we=R57G%a7}9=XglQp@Q9c$_)|u9e_)&LLQs(4W&LE zW`!var0fvkQMs$Kg@Z^rWMp2dOO#vzhh=Ft=jAHSh5$}EPELvraFvJViLR0#10Dz> z;O78(&}n8!j9kKrDD9mZI9uLmtwyZzEMl!g+~46L?(C2X8KbTo_VTi-S?g701+rGL ztnC488_fBj8`B|FPRPCI#tUp_tE?UeK;f*-4WjkCRGXAQTBn@db+4GN3Ab*50I zi#do8=tsw|g$u{@#vme49!xD=BWgUxS}!C9F_yhau+yI@tlVZ7iVZ|7{ISkAG{A(! ziRgJ?4}usVGt;K$*=L@y(R%B=UdJaVqp|A7dtq0cG2*HkX-G zPh4W3+QziaXdgP+bJv{?+;{E^pZ~l+d;6cJ$D^=cyX4|6H{D1P{f5__sW9iQ)C23! zrFY!oaoX+J=XX64I2s+^= zq{9Zn7&Z4?&*8dS2FK?6g9Hw@St=@9T z1YG4pNk35F*G0pq78%lujdFMZ2mB|CqBkBm4Yl-4YNDClTa-aWkCUDXuCzI%N{e$C z9a3q~^4x^vB+g7EG8kr&{3*0tD%bj7E=zkJV4U%Kmti!Qt9eXm%&w@-d8I$-k2ugT`}%Kc>}^Jz!! ztX`h}1@msFAG!OIgSVT)Hiz!IoLludL+`5!#0AI zz15_4Y#pjeF)J-qg+OKJ9*sxAYhkGdTyLEhJ@(u{!=2P%!VJn`qzS_P(xSU7ZYj7v&#xVo5T zh{B&XY*oZ~Fjt)F1vE1xa13l&8*`qlDpfdj@l)Z9fj?vsF$Cz8Gs+?nh>da!6`JK% zr7ZT|<|f5eCTL@h9zd2qd_d8b5SksL3mFJXIjmmxHEdt{QYL@;lOOQVwJ&|Co$W05 zB(@?N9j>^(;QVvn^|mu#ecDm#W&gkI6WRWG`I3u1e(?n#@jUi*mw)lHFMa0P%P#h1 zY%ec+h1u`kt@hoI+(B9_b?fX5a^fJm!MQ^@Wpu^q#?+ch3tB|cfPsiRSDci)36e#O zuB91;9y~Y)bHugLv9(Uff$oMhkea-K!Zc#ZAqaHwjSox%NNZYtCXRs= z{g`D>>ku$quC$mz&X8ge5l9+(vWR4vc0J5JErr+!<{?f_f|fLcpUy6r#0BCp5@$Ce zar%pke&!JKR$>cBbkwra@WCl(+F@?fjG|uVhJVsgwEpoq=XhAfW$KS!@H|%)PnK@5 z?#ms%;?xsZG_1C16waJ6vWt&ipi4oG&IhYdJllJ6@&(VEyT{H<;N;Bz&)9u{jaAio z9;f#%f+#Y82!eD(K&40*se*_CqI8iaZD`U%hXg{4^co}(AcO>x0HGv+fW3|5?##~Y zl%1lpIy2AC>^?jDdEc+z$%bTSpXWUHIrp6Z`L|Pl_ndn#wJlsNmPj!Li0f{!iGpk= z*J=t9r=F`9Lh-bz%@)Lp1JwRq)n>O>T3~8=Vjj3=bs^7FV03c%-M4G#$uk9Ufn(8E zL5PKN>6YJeoyNd3^*g1|$3KQjNhwIjkUsQhPBnsCA~3IP)1?)K1#M{2jBn;0K&1st zOEpy@0vChKy|z4Ot}kefyre7zH0}WQfS3EX6b0Y~HO~AVKF~p3*IZMQhHRu{ZEWzH zTy8;+iEA#BSgGG-L8IQa_e_8r?Qhy4b(nMX8(%;)%pwK40iS{8=bQ!~{bm$RnWXufYL21s5iJ*3eB}NDa;gM<4`Z+7XP04^pj;} zJ&MtQ!C12ot39S(d#$y!NyD5(kF zOmh>8d3>!k*6?f`TXmscoT4!oV?rqD4Ub9*nDa0V2@+vcy;v3}NI@^Fx{yc&51^4n z^xy{s(h<;*{uXAf>#X92re&EeF32sY7wu3jMCLs3w;z7MGVfph={MlpR-0|&p}+^% zTEkzsmS1{_gAdqO_5JqOf7kj@Lxq$9WLIaZ1sl!#@3WU8JhSF|R=M`d%e}2kkZ)oi zxbJ5U`0U;ief!HHZHav}d&^BXUg!O5?zP)4lOLOCx7v%$7P=TZcm3!8^-q5LkH21N z<>gK|@t9+dJ?fhmopm`Id` zhE}SEO0I6Db1Am?4Bk^vDY-*LF6o0Na^D^1Ra-%~=rK`Q_@|pvsmJPG=T1J1RwkIeQYl;f9JJn@4P%g5p;_Bt) z-L_iG%gYGh&^YaEbJ^T|_8F&01R>GD(Ne%$>j9fMiYE*9gbJZZz&Yp&cVkE&G(k=h zeHMeP6OKK~-ZzG5B;GlGobPizeD}9TBDak5qh^Z%vThlF<9+vjYwWmdZ@q2YU3cGp z$As}GpLXJ-lOAG74+IDmu`Gc`;ZTNHJSa=^;3`V%XRx<@YRZcMt+2Bzglpp%aJKzT zN!|ebr7O#aQW%028o?t1C*FO1g)Qftc`65TuNAnl+A@X$3>Oqv&=uU~Ln7qjkEcY= z7XgIf;{GQk4jKc+ZiOJT+<-G2?JL>#moGf$_>+!3#QV!vTy*+1m!Iu>U-0t16K?Q7 zsfqX9=96E(_vJjc&1Ij+Ho03nS=X7rOflvl!Bk>4AA%I!$GoHan1BS7a+`VUQ&t5M zrI@WuMv;k_&70TsK3M`jI;q6y&3AgJTMEb}2otwS7jau+>18P^G#g@w`B18KMMPwY z&@JZH4-%S1}hYa&CeW;h0@RV!b=QoLXB6BOGl0*bl<$)H@ zuKY|K;)bIN5~@{5uXs~Amnusc>mOuz@)86ZCA6j!Y4Wt_T=KB@tJeh$M~` z2nI^GSjmb&gp?YCIOPQqWPD=P!O0l%wkt}8P&HT3R<)vU@mzH%t6*NGl3U_%E0-&T zlo63`yA5vr%Rm3mDvmfuGK@vQg^|JVve5>A0Nd&~1Th>?74eAxgN!$w4P3e^>6~!J zX(vg9mzQ5=af6QrMh6P0aS_rv7Up$?6`H``_lj*v11WM@ghPl1;?NUK8NjmY)3ptr zbg=P_m@F1?9sg5UPiCnA%iNfH7>NIa-f}VAjcC`wwmX&`LsVt{eD*sEQ z15J1vr7YrH(We-Cxmc-OepZ97RY#@J^9kKVq_}F3B2sX`2V1u^ktK-WiO&!al#U)N z`XC@eS^m$2)J_dP!BCLM3Z*D4k%p{D5T4T07Mb=oXd+9xT1o?oUGv(gRhG zmpyx-m^KeyLBUM6W$ZtUtFO8O4!oq`&;RI0bLP&T^}=*l;rSU;r#}1S%vsZZ^3(6H zxBlAet~YW#_>418`TJk}^@}e(Z;Ky88_lF^JUWGg-;QLT^^KGHfj3Ao>+wttgRy!L zVz;DIQp2M&>C_fjsvrZ6oTg)(8HFr|Akay9rVj$>x%wC8(itlqPplZ&fpAhdQ{W|6 zWH=K+ft<`aXt6u7|>(?H# z>Dtb=>9FDstH}NDzxr?Me(-%>lETe;gW3iGFHG^0p+w*-Ir>H?0p*qzb zMChp%F<-M8`Uod*&VX3aut=!kahe5~_KA$KEMln$yD&G>CPIpajo3O%eL`XqEDCO}7--KAnf&twsD+`Tui9#tNa#K$( z&RCVgSTyJZfN}$*B}x|=P-ir5h4?TWO_~o9-Nta>i8%U1$CCm=3Z#!*4$-MxL-*=M zkMO|OqgvOxhHHsEywy$WJ8nlF@c#agKw70dqueeq%v)I$$Yi!G%q~r9Xsep2UZeR* z)VefI5h>kjby{GJ_T5)yHCmy9^$P_-;E`S9w@tny8_KRiwJY)^U5ZP)trmaBj2qbu*Y=_+1f-e-po`t}$2 z?Bm#{9q}>W$NtKJn;-jy?Y@5GuFhkBY|}MAyv|C?E;;h>dGRGif5E)y;)^Y{>>`UU zVRkTqF17gLrfk1%fkPc9_Li|}#R)FQ8qG3*v&jVrg=)b}6BFRpVQHuksDvdUYp)if z0rU)Cjx{(aJa zov}29LCq9U3ZmR<;|)(b`l#^obyr??_2m~^3_z^o#$J2-~o$v@L3U>@E%X%Ng_=Ea4Dp8%~G5>0cW-2 z-DJDTHkWU|#ToLEPkW7QB9HT3GTYpaaE}3^!_iJ?JCf~tU-p+zKK@W&{knDRr4z=F z@$==|ZyLEem3`lYv5sVWf4T2{!OM<~nZnF6x`vL{%&W6B?7J;=tA8~Ci3}Y|zf;gO z*R0`+20{JNEe>wc6cp44O-EBN;}aza6=#^G6l1UhSn+u zmUK>{L`otif+o1h6>+6Ag#{K7O0)=}W_EluMXDO{jB+TYJBKN5&i#kK|LbO6un`UB zF+nEK$}Mh0yB*vmLasy+>EhiMaix+h9Z(RQyZ3I!0Ymiv_@{sH6)%5>x8l{0pk`NVSBBe| zQ++y-L1v3hH+7r?pwOV;x~2x7L%0@fzxvz1bPIYLZ~&9lR$js4 z8CxR*Wq$p4e?_h(y4>ZKUK&rsm-nNUGEyNkMyE&ZMhL(qV9Nxw*@LG!f#bjBBDnos2%2Df}vZ)_-H&t$cnjiG+6OG&Wd9w zQ5Cof5@ktLyxLJsn!2o-f#Qg95LiS+b`3@iDk+5me;&ekm8F57h>&Q|gt`buf z+u8<8#VMc&TPn-X@yrhrMN&pY7Rp30ghVNVI6er_CkP*gq>@~MDjq?#BP9-Q#am$0 zymnheLHcviz&|||LBJJq%W|7U;#8}L)xC5;Juty2VK@R^P0gB?2Qmg3l(e;#56(b6 zPQ2UD2XF+DKmO%US6X@5WtUrG)zwy=HvQ>8`O{yxfTx>h8K<4bGrFWF02{2gp3ciCvnpTZ zHzJJT^%tWD%5R){(tB1}(ep0j!+Td-$uBDOVgr}{t4AviI4dQQT>rjA>D3QM>?~0< z4_4EGdXVoX8?8?bMNH^i>U_J$YYxSEPedge4;zpfXkOq49u_z)?`aV7n&42~;xciRk#0)cCa) zkA^rzyV8ENa~lv+R2he4Ae~vkYTE=BT^z_>i8%^StTY%VOXMiG%IY1~Z<-kcSxN_# z0cbF++pr$kB?Y4ef-H#x&I)0{ zfY4SPhOBub$cBItf6|E<67j@D2zbUVtrFpip%htx)>0I7YtPhGidw?Nx#rd1lV~9r6|h~#pl%=UZ&Ur1L8r+3T94vW;-PZyR=`HinmF9eeP0Up??+U^1`_DSzoRn;o{- zM?quoxsQDT&S3KBUzj&UaKa&*p8UnF>@UO1#~rpkynN_BTW`0;`xl0n!9b9?yr`e0IL_XUh?xiygn|J#5YNicEszK*f^{IC|19^#Fu>C=D1FzRKLLQ7=q+?r z7TAa$E7K4uW+-tRtS}%T3>Dh75s90HD8Pt-R_G0z(haaSiOh@^$)-ZXBi+Eyr50J_ z{VAha?hQkFG*hoFWbVt?bdOZ_+j*2x7lB|%e?7k8@*?Zm5(^;u!#@dYm~OD z?&-0|$b{0(jLZ*S?*h!_!pn4k*}+&TCOa=tT4S)Cb5IfuFI$|$%MvZi{rH3J{Nt*u zAQ27zW>`PcGQT^_dCXT1XIFs^#%ctLV9Q3Dh_FrsTUkV7V&MVAaS_EpW4IVP?k~)4 zp=QAeZ3r5C?k#5f&Vs|+6UJYD?ioiPchmv)mmS8w{ie%(8QT`K&1Ikbdho8BCr`Zl z@rUn*mtUSSWyUj;n!*g=`j;h&8QJ{n))Y`M9Tdz#f|)|PzE&?2QBe9YUyC;t>WnCr zRYW2(9Z>JnizS-!1T(2zDFRmdwTOcCcSS_pmRS10vw3i!EIkeJB!WtA1+!|>Ol0MVN2lftdMUhz|V~#EV(2xksg{;db5=HW`Ys#c5GGQ=;_bfG4pNK?)#8HZ9?fNMNI8Dgs(k+VE z!Y8bD6_V~sE?HHaNOAn_<9GzuSYctTF0==>SYzlBV6T3pXIb&;s+_~^1|M5I-Xc*- zx(@}bQ%J$sWy6SK_JEYCTIs+odYo53BpR|j*AlUEHsa~OJjpH<8p_rZI8Y>y$x=L? z;);_fhzOjZc-;_xoK0$ky2?U^NzSjjrKFf!$`FKS+=y_CCx*3T98@AQ1ejNq3ZitS zVL`ltSgCi?-71^ai1B18C0Q-vRuK(Mah=E@P-7dcLx6*d&|e4@FCd^B{V~sVn0G58 zmy+3wAEw_3G2fUpp30Phax<(ModZS&6ztJoN?B@KU@PiHjzOakA>|l zud?d$uIHY2rd`7S@@K!4Wj0ZeVx)Tpt^s4{d9_65t5y}{N)xw?5@^U>Bf$kKOu^O`LHudz$>;}(K=vby#pN^#&?S>T=FRfh;Z z)ut-~bDyh~iLT8dykrF6b8-ESMRu8WMq9ns8_Y?66Q~i%>BKEatX_O|KLicYbDFgWAZ!7!<0rON#b6!fxkY{XPK&X{gl^kfK?4HRY?E3Xv8F`< zCb;ssMoT3=iK3RX}JIUqH; zz^^V1JuNUmWYU9sN@0teYe4VaR}6z=fS7>5t%BVS=xhLJ2;|Zs8nVy~98#&2$P^q1 zB9=}ciL%-?mY!k~agI5%*vgVF47v?jB}R{%kkE=?D}^iO#gz#{HiS~%Z9?MGO)gpZ z1f{WE7+#jHrwk95MBGZmVO|j_7PmBU&PF4(S7TwF8OJ~d9wOk!E-=Wg-Bu8d<}Eme-AU{|@C7V8cMTW24Klaa>>6JF;dfvC{@XrAIG(|_>B^FMj}g&)81oc(2+%d?+-5MG}3qOw7+`_$#h0X1B<3IL#-#YvEEoRucW7s~CJ@5W<_~Qv+g$bwW~Z_rx%bv5AHC*HVoCP41=uktRgBmC}F3(GX;XX67OV zPgkK1?kXjriBn3VkQ&6{fOE{<3X02(9wlYaNOWaRxz-h`-aKo{wxpqeU>#`cM3x5- zi^PzsM2k105ocNA35$inyP}bHYqGb^>>@XAAJA&tx+AGG_WgT@r=NR&PvNKZ?lQjh62SYz(4zc zc266`c9iru&(=gd{h&Nh?AQ$A?LPJ~`m=)0YRYj+OX$;1K3Q4gkJm-<RYnpO2VRI%+>p5ZGRqh_;cMyYLQm%qzTz>CVtdVOXoPA|1K?6ALX>n(t%808Vd~oAK1+_GZ+{HAaF~p&Imz&&rqN`RXk-g#q5eTTN zl!)P1Q^Y)n;4fYiO(k*6T~i*JpezpcAl40PNWtH&dUwT&2*k>28)}CpJgh7xS%)(n zw0sRfoHt~l7jR~skmk(SPF&sQGz=BNhakifQ%PA&@`LpJmNKq1l!yr>1-GsmjJ78Z z;?>@8)9h8N=1ovtOsPSTI>p%)W!kb!uMZ~RCRZOfC27N~P!Jgo-i|ldXm6{mxEzNQ zP41l0cgA~*+%BQLX6VMI^Eb{o8B$*RgKN49pFi}#zxc~Pl~rH!^vNWpl{QM4$hfzV zE2?F#$VC*Wsc0?3=tMCJppvB^ISzrB)mjab6Qq#B z7JJalpC*V(oDcKju)?xSn{9c?SYc>lh((b_h#hUu!E}E!H7110 zLaCH-$&z5>4L7h7FylMHa?t+!5^X%>C6;P(ELG{eL~Y)dSi3?nG#LJuE`>@0_aZh0 zYv;0RNLneA<;GBYZAp`kXYO)KmKtZiHi?xYIMIFkSohnHPFa?z+XC&&Bk zL2G7mN6#?WiGIz7@+=L;uNX&9nYMj4vf3X#T-wS%L_i#6$R$)@FEz9!`qYr9!5r3^ zd9@;|J8|Y9bLBL-3Tjk5=2k>SXQ{II3%SKrki{WdqY7d>V$xMEGR`SrE*<9~aB_a; zYKl?Mts-2cxE3g2BF-F*lmE5S%;97nCRCQIQpgBW7NtanR8U-QmF~)}>IyTWdvBsfbDUsym1d9`t=U~oI67BHOe z8rYN=9K%**-Z01w&{ceZeo;!4i*jI&fd1AZ0$bQ6CBgAV31pwFE_C4 z{pA3%{bjfqWOmyz?2c!b?)OsmmmSG|$Jemuj=Z_-Ghg3%cXWG3`vy<45+(_ojd_jBE!PE*7!T983n81-BBz%eJ0|R3bRHOq=5FDOx zCI0lZKn2)gOlu4&4p%vp`MG`ey6wj6>@Qz)#U;$KmcHFSxZU9!uJWXP*WGqpV#!6| zAC*F65UobyA`4LxBo9Tc=Q;gBuz}_^`bWn;}pkPwaee^Ok!xW}Vc8f;D zJZ09yuoCBO)h9+USjFVMURgn2o@N0_Dpc42|)^) zm9!$kq#0zUp7~d9voLR%OeL}+RjDm*dNrw;oz2mfSemyrq`-AF5oD=24TkNQ5G2cF zZRx1Tsd2ZMNO#pL1`n{2Ae5Apg};DIx*}F|nw(8l%}JZ=s*i|Rrh49`ty;2LyW)_D zrd>QKOeKpGmc~|`o;0L@z+_VRR^S0CvJkhxG`*+L(2`d~;dCW2e4@)%+~^MjfNMs!AY)%$cMN1%Bgj#j^F6J(siwY+X%5nqY#Jmga~HRIYpKo0ikMyf}siALEQ+2SdtJ zQun7%LkH&etqfL%GQ{@2*=~?jt-PTKzwwppU>cn%%&R@4z>os0&fap}sscqwlv{h_ zF^RIIKz@@hpyK|u`FCX)%m6Cd$98e3RFe_2YoN)ZvNbUe}HMqHNG zbn%o}St-V96A%X?RwO~_Gr?6NKVZuPOlDV{(MxBS6hWLa`AwEcTIt%gE2XLz%w?%i z%*B<}ss$dvfiuk%`lC)Q&P%jaI}9&~Bg0K`k!~@Rf*6f-tRO5Kl%8~{FOHQ?CG^tG zdZT!G8OBOfYR1fGZ4r)c{LKvAIGYU@3tMlq8Kk`OD$BX9xY9BZG)fk!7q1eVlr>b% zRfl4cDia2siOZ7&C5tU;R-lh8&S|AYdDz4tHjHD^5x_nUIX7$TDK>4LTa&IM)0~+# z2Fw$r4?5A|xt~YP+5;xEs%cr^Ra2DrA2$HmfoGa~60UbctkhPCo*as!)cbgeIK2-C z{9WnkQzuGMMpxCAbZ|;q9$3i`hk&;&Av{upf30@N#WppJYz7cnqes5i28t=kliH!f zZ$=XJWxU1BxFqnfEboutNxG*5bsyf-fW?QFKw zf?g>!Sg99f1-V*O4O>|R46h%_MaI?4RB^RoH28qPwHA@^vWG1qp1o*P!15E5A9aPM zfoQwTeQF+3b{N~)Z2MyV;AXWS9PkS*K_Up?3LTKx^1I%|rQ~v*iG%ls0fE&FG#|Or zmT9zQ9iwXhZfPqpO8dcq0~ME2-{cmY<#Iya(4Kh`v2@arDZ(Lw2oAEitAG`KbY4v& ziE=5Azp~U8=TfdyDZ0Ew5(R8YF@zfWIN5N7d7j4sv9gp`5M`>mB4DGdy$>d{R6Eg_ zai%SLoQY6M7GE_))49*q^<{F157>k;gMJ{bxiFwKpeiV7fdM591uvr%S5`qN&vk)e zz{|KPSO|heKcpvGVsb?W7%MJU`jA5OfR`<#=EKVa&%`M!f{9XL(zqo;B5W%zBzjOl zN>3FKFeF=CB}R;GYKtf(@&ApN2M{ent`G-W4F)Dlzkq^0P;#wPgrI}T;<|}2JYtI5 z;2)Y4Qt#c@B&K%mie_PUX;fO8X4bf*N!OlTweYg!>a*HyiPHbWfU;l{87_v7=L5(? z3cL&~k9_HCG>n4f7vFk)X8X(jB;`Ez8?&Ezb;hK*FHD~E{I_R5dGEAI6X0e0%fjS) zuYU5rYlKH9T=v4`+g*?S{7y$5xWi{Z_2J#OU1!&A)^;%4x4(d8sM$-+4qywmn7?ws zHp1ciY#w9=lMme4C%;BOw6~jKYGht^w!i$<{Wr9~e9XaH*XA==ee;wH&OXjzY^Sm>yYSS}BiUo@ zEVsYx``FJs{y>|{ug;q4{bfVCQQB-`TG7|)M+Wm|G~;;#yDa0p-iJWPk=~48cT1R>)eZc#+#8Y)$vcxntlfEq>vnp^7d0B#nm zmY{l~)xHI%g{9pbd%m`VQMO3HA2Iz~(ugR?E`fCFF&P0_LLTM{(*O7W{AW*)*rM;S z5!&^`CNxS}fm_XTl%BQn3M&w&hk9?Cr3xbRNj+~MIx%9=Za8v$8c!9CjH+5JLDbVi zq*xW(UGk7Y%Q2w6cePb~%)#l(RaRWqVjdX|itD7hxlvIO-h*ZM%Ig)*itst}b#UF; zikUcwn#vm|6A|2=~!W`A4BIs6)RO3X8xZA|($7)~O!&AB@tsv$#{z0z9+Nbn@q3Tlvfg*UijT>`t)}mBs z<3pA17MUUBL@n?biEepY=SAgNHFbXk2i_#bP z7O@_oxWIgMfmlI4aYd;hwdE?#W6`jVP`Ap~L1IGr@UoU8(kslaz_Lc=B=(eNp0Hke zcG^>~&3Va=vj8cB%)s(wFB2~h7nb#>(7l1*4gbo_nb_jq{(JOBQqkOgW6yp5(K*jegpnV=`^txJyLjRq zmx|kE_LB0~kJtk`b}AcW-g}4jf#nWkAFC-q{T9g+nbtqdVbn=&w+aobd7U&iEa@hn)X*$-06WMmC6tSj8Y%jl(e zW6NL#0K(3gz@gxzV66*KTb|i{22s6-Xiz1Hp5`x#Q4+C5thJLz$_`_1zvX5V@4f5! zrzc-?#U-7O&Oqh1bL3-cb#4kO!i z7SI6B$YAR5@);)|apoyUUUL4)e!)EUnr~iz<@pcaJKh$v&E@G&Jz{71#pzGJ`N|BR z$ZlRUTbXN2#71&g(^K(g5Lf9E%|SX&V?F}G)X*$ora&Xj)U@86RYr zNXNlif`(RH=3-2&6*WKYU)!)8^~!_hs+s9%PDfa&O0@ozYk@}=eMq4|-ou|eb{SFqO27{wy@U_YCv!jl0-o(^&Cm3Mh4GPfLz|doQ=fc zqbz^2i&1z|iG~zNhdfNQvKGBwf%J-E5Y-6~P{Q!82@VD88Z$o~nM93O2AiGpY2|@~QqB9G_ERYraUkZ_ zlPa2rS?5ue=E3CFsIGDu$bobqj!=H)ZMG{Spy4`hXC1aR9%@sGwkYLl`RR#s9-|d= zaF#+zE>e$bN^zl@MX9V!WMEm)a)x8r5}%nm@+bymc9jc61IwdZ&m%7{PxIoES#rt~ zfPkH{e&6PxDZFguFQwfy5P(o>2r0&*v{GeS8nu}~H->YL$wG=-8ZtpyaRmIMbBRk7 zu#zR6uQUwlJj`BM;u5)eC3cqS!Z;eZBb9vjhW#&pi| zXc9CArov^wk=uflaGcyg(*ki8NQGdabPIF2E|6H_z->NCA_{0^l^c9>ZU0(X5Ykm{ zY6mS91P>J)kY;F6I?9BGK&1?1;?z!-M7ILwm6avpR#qI`N=&-Aof|;}2Z?bOr|sZ@ z7|Nyg5Znq$RJ?SN`8c?(Qe$m|Pa8Ibx*B984k<>k(zQd03k_@{TlnsGUUVe;o!4i+HRpNfu|a0v$9{A6({H`>kczV{Vg zb`tyf$HqIA4K+Xe$k=h0p5(PbA8C z(3zpy4>gfl4|{@YGV-lRBN$>2jp0PV)*J-)neTLM2vHGG6peIbUST$MdcO}Cw8yHH zfDc$|tOJnYN&wST+xxkm*%C*z%~_9yfkdLwTY_44BC`aQ1%X24odh%HP-D$iSKVTx z4SZtEA;@ptdecqU+j1TGS+nnB+hX?q^0)51_2b)Zwe&KJIgh>V_FLIAf@-l{VOgiJ zR-vjLaH8qs**hz|=V(9!&%MNLA?=}oTQCH~w$Fl|TbPxN~Cbt+NiTcmuKX z=axMlGBnF$Yt7HMaJMG|KA{Oav#a;P4cEk3DM|4)FoQ6kaD`iY>*%=-L$~i=b4?JQ z6a0J`H173eh`8Nk*LmMxmL70+z3*GsUVr5|_Ln=4eZy7f!OK@)cFz5Gj=l5NYlioi zr%ZZq=G4cZdHf+YXt*}Tm=uimDofneEYM`pyxdG=#&2>g7lBAAx>BhNV`NFIr zlp?VS#T9?0qze^qQWGC|DlSM#R%C-##OcL)I=0P^IG{&B;c_u2T}lzttEOeKsus+i zW?tzuk**@}F-}PE6dVLy^x|fCZxOaVat0XOcFE1EUDefWi>L) zmBqngz*Md*CYW1A=vL~&TpY1Hu2%Iu`64f2^2;tsssSm}xP_ zKm^rDCR>9cV48HYTr)&*=_*DenYDrvm(tp==B-@`L`aMY>li|!Va+22>r%$zsp7Du20@q<>L%I9 zGYF($MY<}(R>P>Q;?l`tpez3P$km20R0+jCW{)1lS>nUwirr8CZdRUd^{A=t~};)V1p}? zC89&=U!IJqeeF#J0W#h*ZI=+tvC9l5PkQ_jfB*UJ_ul-Qzx|6NzkHaFb=Y5CcfED} z3jUOwj$QA}_9@KEvkc^F3UGyrRy*uvK0|~mg8rPtMd|EG6iX$tPH~CaExXY0 zMlQufdP9Leu2HeN-ik~FnSdt_Qs^d<2yGj~{Ew0j*lIJ#1VU-7DGV5FeD@WSMK)}<1})*rcT<+KF#ILGfQ-Zec)A63`{NQp17Z!U zwmYm~pwj=O(2EE5#i?hrTM%)JLk!(2g=~Pa;Y0)lc)Z+hk-3#vafu$O2qN<$@j-17 zltc{02SKGoAYQr%`Vw6QDJcNSj@acp>H%Rd1^@I19l!vPbs`-^-|1ksh+BzC8Nlx# zeMp%vlB$E5nNI=fDMpPjb$01)8eW~TQL&+3+mod=x|Qx~_^P)`e|Io)3R7HG@^8L4==9j)V3YmTE3uK-%>xs8sdB*$8uTGx` zFZ<-ztJ5BK9{ZVxZg}ab32)AL(Ep_DFW-LkH^5{V*~hVW+3NkyV+WrhWdVr(+F_sa zee5sqx7E=HZ3_`Y%FbrP#kQE8#O__@&uqW8h@;x}mk-2*gQ;Of5~#I3|=(q|If36v##9kP-qTk4acPPAY<;VwReJ~m=JBV z+PNl$L@?Z%*`gG#6h};!toN<5;vxHfX7VEsLh`rYeB;<_ufzoIzT@_ruN-rU{bk?B zKKq>0M-Hkjw#dhK*xFLZ&NZzV1#1}M;8Um{E9yA~ZK^5-=&)r5Pf>+bPTgFORY`3ZhjVaF$fUO8-?{zCs> z29{4f@vt!$f5ZOrr5Bub$IVwdlI=s;Q=fRydF%)7^cM5TC$jxXT&HNxHU8^mI)zEW z)Yjl%zccrlTm%!4?&GQm`j#1Tg(jWKni`rlq?;f{d)Y`|b8E6tRxM!CbY_YX6f7mV z;uyNRC6^!t(IhL%wAJOMv!eOP9EzJP^hBe_fzAd8Yge_S{cC9&qzP9M#nGFWwQ5?K z$V^_AE+$sFvYM1-SxG8Fc??}mbBKwirwFnrY3mkEv!R0KO1_dP3tN0NIcoz(b*mtu zm?syprmi>|-Nyo0Z}nhDlknDq=AmzDq8``uD8@n#4Jg;lo1?gh@c$^iiB+ zE!PnkPqcO^Dn$jMp+J;UEJ5`eOL5&$m+={~-~9S-=t&=J8Ddm%tkgP|Xaj}qD$3(V zkYz2!^lq*o9kgeQT*?rk_DQib<>7Qx9`*j||BG&R(PF0ZL*`I>>CF6cv5Tggt4 zVyl`oFx=>4AR^>j6f;1P`#tcE~a59 z7at6}R(HyZl!zhEQ=T6@Hgp(B>sSmd*m9Qkahq0%V90P^o}zAq8ad zkA@)^@nC7@V9RhEs#!Fa$3);PMNnL+bO;3?#-zEJ4vChiET?frtdvrT6<1{%m0Oj< z(p*skW)T_^)uRrDVa_C?fru4JuY#C{Mhb2jPAj7084V8X)dlLvv=Y!LNIDu!&=_8j z$s$XZ2uIO}{P2R3JxsGb$|1%C97Hs;+7h+%A)PsdXpSX| z?NntW4e8G=+R{y;BKo*2bp$NquSd9=>&<$YBhy2@cU+|y4KTq!xvn_$@P{9mU#$_L zE(AK{G3h$8BV-_LhfH`CD>cFunwBoF6uDLf6tLuwLJ-B+B5q=BXfiNLr#8_>vw8=d z%MS|LJ~lO*Z5eJNF!JyT6BvT0G1ePf_L1FkpyKK)ysWHI*N48brHLw|xj5+zF}`VX z9EDqrjcFnRWh62|6JnH7+(Ij*C`pa_f?Q;BrCZY)xMXQR3aUpnh`B_8uW(RnQjCy_ z3))!zOuDivQG;<5!BZCvNdz>u%PjcJi}vG7UmnHSdj8>${3Z5n2zuh9587OI9($4x zRzLBm-!WU~J9OI0zt=FO2;%TEi3#NpDsfj;oZ&nmNEeuAlAE02d;cl|B^0fU<#tdnOv+Sx@zRAi4Te3e<|3~0q22Cb+}D2 zm6HA$C0zt_CTNhViQ5I*R$rI|v_%kOg;-Fknvzbd9EEd2OQ8(iv`p%;*Wv8vratX6 zB0418r*W_uN`i?%B)G|~5Y7?-uVleWA*slGSt~px9YIB;fRON-6vbVopa)w0omm7K zwCmuvEWpq;oQ_E%<}E03fdSlfOm+j601lnV^nvsP+|!w{Ll(AI;Mm3?FZCSk2{k6=8KQN{qmD<%zVrzzw9jg znbbXF&jp;HxaTU@E!JK3(_4SQF>H|8Tg+`MyZZ3g7x&r57rpEqkGzomn9Rr;4 zV0md`GUK@F=5}E1p!JreiQYjU*X#~yR#CAZ&k z^R?HFS!v~!)>(I*ub+AXOln$(eC;peW<)amktKzCrcPrx00jt}g*}-!HCx`>P4+OE zZYDt|+!O?vZ8U@YcGa!t57>8K$Fw+*$wW}hT7nbI!hULF1G>wp(v)US>7y z?~u7kSvn7U3IRcjdkNG#n>H65mEUXk-925GD4>7vzT3g!KRoN}mtJ%>unZ{!#aCT% zfw z7MX&rE6^00!V(mpuBAkS^Ss2(KFA~@bBm{|IQ=PLW5Ud(kL%asY@tR&1WIJ|G#_ct zo}XCPH8&~CC|5JDiqJNc(xgQ!QOsg)u|i;8R!UPbMV5}Oz-j>*hKi(B>5Vq6k#JCo z!mUfKa7Yp7oU%;fL}0=S3~6N*Cl|OIZ63>f?tuL@MeE63cHGgUEgcGHoy$1{1lD*) zGa_)yIdo7QLxC(=Qt~tYXoyJdtd>FtHX@Uy7ciEFhF0+&;{eE(%64*{Th?=R6Mw)l z*m67Nts>M8=2Cb&hFGp2ZPGbMmU@?EY9P!0HJFUQKz}~b4V9$~r!kYlEaF(BfwJPd zyuk-s+~m6Arv2ccQ$x+VuxEYN&n<&njGE@FsWSH(F{5Ghe{1gzjF?Me&xT#m{33+kO3$<5fD#-GD_k7x`vJggF-2TUW)T=EqA~l(HD&gIKzPReCyCgg7*ugA9LJu9-uMETSu(f6Af; z=`}WHVue_i2tnx6(4v|*aH#!f1u}ZNVo25yr$FM6MTr2Pu4Ea#3B^34(95E*mWB`0 zT$#+J%wcTGh*K#k$_fhV9W|jfS6>}ohR6`8J5E#kz@XNpp=yv7xBUms^xCorL^Jux zZ@UUV`RVt8AUlJ<`R(6-W}n^7@*90*BQNl3mIegx`P#h3#Nb9msEYpL)dEjao2sm! zvl{0s=2uy?rBxo&unj(zULF%USOSf1w#g=XiYHnITP9e8G2bI80b3$4DZ-7K1P+=U zWpWowabu&P5)~)Qi03Jmr_)v@hPV&B?|nI0K_<`#0q;4-#Y=KM{xU@1lTp@k-pep_ z=$NK;Ps37%L^|M4PYS$uwblHNij%D<8k&qea_R5;(A=5CL^>91xDZq^SS^H$1E z0U8dJE|6j|$qE+XRx6U5MI1|E%(1Fjas?09auktX6Vgtk^LACkdrGMq9*|CL=8GVc zs9=6Wj3LcsX`e($Q8p%OK|wpqp0_w_-RHczYDjRj&wTYMIqx!q(puWLAA1N!2A2Iy z>PcU8pZcVoWsCgw$Q&{4xT$S2LE^~H+UQQVD?nrNyE|hG7<|Ys7{`eC)`WvJtR-kd>3kq6tTd5Fsd~>k)x1m!_>N&M2c% zWr7+P)SBGl2;7!}Qa7QkF@b0riV%uYGj>IQURf#$iEx$91c6opO1im4!&v>!w55?7 zXW(T|mjoCPsv4jlloSRGCAki;E*KT=6BhzvK`LZr!Be5IXu1Xlf!RvIp5bL>0W`=i zadH)h9|LZ2Uf>nO1qP6FkY2$-jes6;LN>}uG>LZD1zC!_Vvb$}lXy}I_M|x!$z7fl z3q5CG3u2TQrj>}>Y{C59A!VpBC^Hlv%E}!=pN{X@k-g>ufNZaM0L~J19JkVOOG&z6 zl21gWyAnzPs+KNM)nu|dZ2xol_IXTJP@8D4h%_(rSrjW6#nJC6-1_e&|) zBlq8~zeG8D?Nm0r3=~7kZXsxgwB5Ebl&DL@izl!B*oQ>0kIT9+E!Ffl_}2D`$-fmSdX zP_-vwn_9 zAdu)+m^`TN`|`0b|C74v(z9`3-Kw~uug+sn(Z zy)d%fY2-Jt>I*`1u8!6qZ=5fpzsS{nh5=tc(x>W8%?_pjWae~C6hs+Ai6%`n%@-yM zJmZ6G2&FWIAwV|BYJQ0_DLOAH*jf~kXgR?OSr%8)$u+4dWhfxslqAcQu^Ol82>}k~ zKC`W08uh%|^w+Yat8{LmgR3k7a}8c8Wm5?s{1He;Ps1ib5d=(@MH5-AMMdaoTGUPm zr4nv}b%GRaPdr}{M|o|`YE56j#Q4T7sz77Y58CXI2sP5t(GYlrQQlD8!j|;&y=iYakf;JrJW$|YeJbr76tN8 z%CMK)sX5B8?Ee9;#O|na|Nsk1TN`09L#q zR}a+-Six_Zqu!B`h^JapkWZMeC5U5-fY6*HLPLJW9I-fy*o&Ax9z61cIQ|L>qzoOW z6jGFu&RwpERVDo?GXyC_Bcl}`ijzX3DvpMFnu>V3(O(uCI?#%$$yFRHWUc}-(q##< zkf~Eeuu_~9iLOO(rZ63pN)uOE_>+#O+YGFE;3kpl2$DsRbP>hTOXoT!OGHT!w@4uc zWtwmWeL`w%;baAILrjo@A~B?fA{oUNOk5GTVk@1mNJNwJO2tI3vegIzhPNQcK`B|3 zkvr&|yJ({Qm|;zz@TNrlh&Z)ol4?%TXo(x)_Qoq6l!bBZ5(3UoJw3_Q{@}>huz&Op z1oVqP{>iGVtzgP?HQ{Qv8Z9tx()nNi@{jz$aDEnQ9{H1EJVq!tiql`Mxylvt4?|=% zUQts2GUkA^Mp4hoZ0Ra)^7R3KvmUf-P$Gr$Tvr9*$qFWG7~5e4p$2P zrMc0@6{^)A300P3RV8`^kgOqsM3!O#MB^`4q&Zz#n3^&%uW~(VLmGz-1o8bl2J?LQh?tW&Mvjd9D=$sih)E3 zWRc>k`5|BqSBS_;x`7$e@DMK%y40<$*Gn{B;<0(tc(u8zz0gWrIoJ_E%+_$_u zS`i5>Pn-U}PfVnbW`l>wMd1y^k4o29^hyKofzsl*uS2 z2ow-#%LH19s7Irj#g(VB6w44cB11!4ma0b<393e)6*L$$IIx{FQ7W!sn-g(EAcev zL|KB{ArdMKh@liP6hv?f2D%~x;xLSfD~5wOxpa_%Gc*_TcVF>YUU9`%OG*JXbRz!fE#Zjhk+_1`!K>>Ug7Alf@;wd&1kAqxUBe_m` zjDFgy!`Pk278iQHG+!Jy7PXI00c2`O61v@0-}rv3+FkDZ|SSX1lea z3^l{c;ByGt9<;ZZZAPE`#jQZ*Q@*_2ImhpQ=J9(Tf7p)BWAC@emLJ_<^<_ujdHi?0 zyx5ZPGDK!i#BQc#ww=(P)S0_YL$D#lXL$_3nv(368jpmQ?QtYk0P z*k$$Zr5<3g@7CIlRSK?b7uBP3i+QE6g|)#tu1hYp8$C$fF)%P*zwn{dpOJQQ(TSh5~DFaHPh)(%}Q>~)MyaM6_+wh2gu?hSGph)+c;oSh>~2= zi(?|9+v@U4xs4TNNJ%Bbf{3*c>|-TEe{-?kZBj#z6-rYQ8nZBCwIX{@$OMMVqO)8z zr5??FQd;Yx7cpG8n$b+*Kiou0)y&yE=D36Dm5_+ek5IZKtwODGwFL+gAB(33J8 z;!2skCn76fiIat(+*}Q_W1@d)jT@}Dj;rTCMhv;E)7rgUpfOi{c>_J>ydjZL^+gA! zG1KY-L%K1LdT1z2ZIz3ml`2-=FSb+6DBmtn5l#zm2%adh#Zzz6i+yd+%ADr72}Bsp z^dO629%zBh5GpCC{gA?$8*jLwCsf9h6_#BJRFQ%qww!>8Dj-PxidZBo0=($R21dH^ z8xu4D+~`kF1A@RO20%hpldDpu7*-?GGV?YaYE)E`BA317iVL`P!qAmZ*vm`QV1-)d zG+NQHnyouvUS&y)8-``MvKn(Cxr$@UR~QP^L(E=r7D*RGNT*3vsh}c4?!}N@`3kr6 z;R+(s3d3AYPc|wp*R|duE{ID(AczQjf)x?2L?g%qK9Q>k0zm|GiDZdTRY1>hDhh^+bpiW%Y5vM7-L{D}stNL`-2o#;eL8Gh8EXTX5o|4}i?x zV*c?@z9;c#KmQ@5>^$~SUpeY4Up>5?P<%x66q9vNmT;p3_3)$v#rTT!+{kvdAgj92 zJ>^QIc{VbiO3q(-_G{<*-~R1ib!xk-SOJp80gs3cm8yW@3?$Ai7gfJ%q@fkJ`UnrB zsof@sL>{2C=~Z8L=FKB_iF6=cam04a`3>6joQn614@Q2h2&Ig(+~#zHfT#Cvc~xsC z4u6&srz2yoxQF3NX=ZrRjaH5G9xr%=&K3^zC_~jyz>&GmhkHKenYkyEgu2Rt-_dX^ zR=TZDgcWfNr#9beKNJurNMfx;oHGRkMhWz+t8vQWLy3Y2opHvW2NXw-KqBSkx|J(P zK|@NOM2SqCTb#JIaojO%FT8`y5cF_FL5tFgr2pv0-}ThRk!-uh&XGD|I@2>24_Yx9 z{TFk%05kxf;breHyQ1WT1qSNnJ*pyLtfr;S1aj#nQ5Ma`U3rN)-7*BRNzH_)#k$Qj z&J+n+YS@j9_^4&#c%Bn@5>1Z6pANEkgO{`iLkCZiD4-_}C3lI_FddX42v21tDvKeu zDwjn1Q;Z;rnF`(N^|~iMpN!peZ_j?2$Uu>?(MR83u%g zT*FEr5Yf?a}ihzY84g?0fkStB@Z6ikJ=#1#pMA;T@m9A1uCdQeXB%AzL`O<+x& zEE3U6M8Cj5;sQf}P7!Pi_zYo46e*Fy@k#U4phMQVWip-^QqPfcx>0*DLD|&C4UJpG zgAOGwQ2!K&-qmg1j>(WNA_bq}Ig$=i2qxE4#BDCyR_=9%5fJ_Mn^bKdpDi(OADr@T zT?Z-n)0we3JoCwAud;3tr`TYfb}Pu&hHL!wKDPyl|39GyjM6}-2Wh3+yiNoZ_X}ni z*@kl4&9;&yiaV0+U(AkSy9)h(`R6~@_FkDi>!}x?d)zOW;pKPcJm=+Q0bcgKuUDo& zY>PR(Y=7B#>}eCn-FoF|Pu+j*x2`+OYs+?);bm7F&5mSyg;}uCY=0RXwrdO^gUtOj z%7(IA=vcZz!pLQQGsp;ux%*2VkJPzHkZAkD-nWr z($a%p2eu&s8Y1Y^ALeKy3B(O*YhL`WX{gyj#yZPoDe)l;tAIG-EY;*a_ zt1i9j>dQPO_=EL7F!8Ym=|*kaQgkx~1Ely{V4G5{hi(4S4JZa(Un|(3oPXyyTg{*Of!Lk`IxhvCxLE@0q!y?QU-qK zSc`L-Sa^GM?>x5EG7eA)0;{Ic-F(~0mz@8HrwVYfePr(`Km5Q1`_6q68(zNR(sLnY z*!k))7v6aFxxV*x{8tV<@62PyUUR{v7o6gIUpHNM@k96AY>WBX$qzkv*Uk5T>*m*I zKj-_{FFiN%+};dgzA%jISO7n=|7Fkn6^M!dqk!Fq9x-~bU zRH`5anNkR%sg#uF5%f)GZV@Q!cHmqbu^F(b&9e>_t*JPer(ZWMBGc0ais|pXtwz)Z zKbt*O1Tw?|J|;d>p?T2!RXXeV!K$NKm<}y$u)+{q1<@eDN|s1&kxOkh%9SFH7+X?^ z5JVKgCx-NqLV35=)rwGn1I5#X<`hP+#{qim(vme`M-Y{=8l+nba-|5uN6l)P*+M!3 zTH#D*lvt%3ouw$nx7Z?+C57@dMBn2b4H9zHBP=XAP~X&mwR4>Uhel=`6g0YuV4ex= z(hW}m8Z>gE7Ep~bwF8|@D)u`&vMrflxbp6;)K@2c;cfc>OmZ0 z&xbQZX-Dt0A3CfXq>yd=U#>27gWmbUn(wp)o( zYH8xpxZ;eJg4oiEtMTD~K`FH?my0-x2)WciMngX5ZK7+RnOr(S*eWF#XGPo&MiH7s z9Eebm&WJ@~BA4Ol@gKSdR+YjEhfEV0+=4(nDb$vwd*Y0qK3K_$rv`>Vx;Af_O*GDm zOGz2Fvs4@t3Lq}fwt$lJliTHXTUk12 z>Shg1%j^F-iw@;sUFK-~x`3HfDe0sDJ3tLwY;)OOvu)*j@B0?WJZJ7~PzY*XZ~e8+ z?|z~9SAYFywg??_!dd&Y87kt*l6FpEfg!w?&#A;%O_Ye-SUYj0Hd|<`V^V{{nzvHC zWPml;(%G89;=xR3C14zIGRi!TObzOHVNc-KJj(c&ZGQGj=#%)G$3g7l=7d6`hbdd53w%tX(hDi?nN83fI}))Ms#oCR7b z&P>F-=gRFhX0i(9=5{_;1UHGUbw+$RU-MH|5m{)k5@Zo7S60=U2v?NII9v;{YwX;< zCiK1U`)Tr9;!a{ar|lTFM0i;P6Xa@O*7+bajO@*2|CSQ*h9>O1FuY8PK%CA(;gqK& z!v*y#r7QpIn8wvcA&KZ!ji79OjhEQMSRKkNW7Ve#DabSYhV6(&qi4Om6{y#+S`8B9 z${X6MYDLTg*wQM|;@HYH;#KdsB^Sf^6HN!UxXueV+$u|FgMemlL1+=k?YL3EEKEa{ zjs{Pm2yUbcB6Op=c9Q}|Lenb}RLLgU&xKaUCBBT@gAL3<{3k>sW z5-J>|5JZO|xD{BW2uebUPE|#?!d0#mWC)PKk0Qwxkq%A&@y~vY3|r}F21MyrqLc*& zWy?)sBDi|q^s?d;I_~IouPqGM;T?auJzwfPHdguBl|R**4RJ!*CB-#`U6U?CqASw` zao-tJum&&~m^lre&O-@VG5ehZM|M04mP9$JrJg>Kk2a`j}U~-$twv~nAc9!ig zgUqkLW@$a+J9B5+VuqJred!qw>g+EA%X43RdSQ5Z#-v-|W!F2eKih}0{aDJI%eIw2 zx7((Wvbd}NNx{qBU$(z&Te;xGHGn(>@Hxl~BincG1!tSlekSES_Nhm0fBs2(pMT1J zryRBWkq7PY`Te#Z#mggu{E|*E|KE7oey-=YOD?vk9pwMR*L^^LTGaO*4=9QVDmD}p z=^zMFq)8E2SeCZ*jwnsB5ff2VL{U(rC`eg)@5s_SyRfiy0ZU9Y(Ugy7WGCT^C>WMRQROwS@^i+w%Y(!AZ507E5OI6fz-f6! zp8fV-BnkF?n53>%s4ZF)JHG}iwgHbc@@BUkxA$1>obR6fy>q^O);GUS4%_Bjblz20 zUgBe4mt1;*zc5c+Uix=F_KBknakTp;Hc>s|#b(nkT+D0cfdgEyDz-{t22L2nQ`|zq zJVPSz?$IyMz*aRELi)s_lW7B89%{ghLEf!zGrnDNLcH;f0%BWfkZ(4}!@FgcUfOhT zGny8b@SSaD2je+o%=`W`>m4-fmWwYu8(xN#J&!E|FM9yn<}%a_Ao~=U_m@Ft$>Z6l zo^s-M&N%7ZGr#oF!*=&PHoSbwNuTiA@`d01hVNrvdchfY-*(N-*UY*8iVL53z#0S3pFCK061%9@aH>z4TPVyzR>G!M-RPNmNOEYX2QuN656P8 z`->2|6sbrEb&hz%O0D9_L7pngSz5@^9Q?w7=v2heRjgxB$1fF?Y7W(l6(PxqGCe3tV%kWj8Q56Kd{zU!J7>#V`NJOy>b?e}Z`Z@~`(gvuCrx zA99X7{O~5zb=F+dtszqO0fZj{*|25FaFW@&e~ z%$usX(rK5>xh_dppPAGehHHpWHjQkvco02?nazpf5y`x$#W5ICyvm`vaX_hxjB)#d zJtU}t$2?*fu|)aTfBr{w_unj9Jdfwa@D-MOo#l`{XrC;wYcJG=h?^W?iTOnUMYTRq zsQE=p6qZQDVn(b3Q{6a#j#7LOt0I6phJ;d-Fc4BLQFX29?T%e3=;Tn1O>v~o}if8VjlZw;K20fvGYY6 zu}#Mia9z6Tz$qIPWsGhBY3kT6PwpT#LSm4?>_~{{i6M7Pc|fu5C(iX%RpgsPtr7uh z?h6GiVnwD?9Z$;=!grF{qn11d1`vyl%^9o|XTcwK;t$UbcTgCh4>QSPl3sEsa*f#R zc8xO_0~L(P+S->Qi5vkbCQKzwh z5?Bn^?EW~EOn*0MkJ>gY%aTwTv_lkt4c+c5C#jZ z4EY(KmQ)YDPVq==Qx_fc^yp!b+|4s6>1_J8j;FIZ){{|2(gRC_vPed0omnrHPavML zp$se!ybLK%UR8eCi#EUhw-m^1b9u7M{PZLKC*^tU=bwJq$G+fYAIJ8|uix}M_G5Qm z^29xtdxiP7%TF)k``9*@?IwfCfHS1*k!rynUrvIrNPz;-@FDCqj)*m zse6WQ_yffrgob>9VosSc?GYOr`EK9J7CgEH$V-9~UbcW|D!(v8Z30MCPoZZ(GoYXnh} z!G>snQflZkFH?t3zN6Ox+!oM(^bmcOOos1sKzt&Qpe2;pzF~k@l(k<~6YKB=kQ|CGrVQ;UfOC4P_qH8-6Y~i`b zNH|^ST;pSoJFQ%n#4z&7i9rxZP*;+1s#e!2 zAu>1>u^+#R5+P1gM=}P&S%RpPQ%=bdL!}adO2t8|Njeoa6!M2^dDVyH%*Zx8iB(tM z)1XczjueEnvxg9QolU7cgK2jpA^D1!Au+PhWq`3oY8Y{p=ajnUbY8HZA}yuQFp^d5 zq=GI<#%b;=mFOZ?Evj{Ia;ic?zB^@rYs9s$-Fd{YB=dqOVsUCX`RrG!h**|j&T)>& zV^&I$ok}yTdDES}9M`YZ(y!e4(M$DPY$QWE0hyM!sv0XKVX@45ttC5?laYe5uZxlb(LrOdz}s*1?xyXF&rVR1?hJT(QD zC-)7^xt?JYSjOPTX0Al%luJ*bT$_9m>e>`?H=h-e3LW-x@0fmI@j*r~uZ)bV-81 zM;0RDk+?r1pp+Sm*ME#5+{_a3AuTplsOv%?q;L^PI0kfmQfjDIi!`N_3YR8^i=TdG z0kG_men)*tlJTD@PxaencAG_Cl6*c6aJHc=1(O|H=eK3nR$6!&7Lb7ygjb@DFB0>3 zB;_fCvxljC6dz+a)LHLp*x?9%dF{n!y`}3H52u_W7C;%HKs3@?XxWI7(ZL9pDB`|= zh+&sGev+Aot|Eb2hhdDAvfPny0nhS$sprI&ZCs;*9L5_58XI{;CXH;Mkr1NrNb>xm zOA;J?jDt0qA{6wr$M8{8xX;vg>x zq(Z?HNDO&}tzrv&p+v+P+L}(64>qbIIStUMGT`LmnIUKK3@u48(gpXXBfg;p8x?={ zCqE;+9c`2hK#xrkqUy;Zwjy*y2^-(>>dK<=oy*8D8?nj z+TtB}dS_OuLrOSx%OxfkLcLc;-7atygUO+0$G-a2m%bFCz|MhX-^!lsJO5yTPh|V~ zvj0iF{M`})Dc2RxGfo4)t;+{4$s{P-=ln0+YQv)L~_cC%NNgUmgT?UP@Cb3bDC zODX?~0*(99mzR{`<#v{VX#38#n-RfhXu3C=kKTLTqh_yj?13Bo?nk!!>@hR?-k1I5 zsqcMFrpy0&fBBW~ec44dnOb>z7YU|==CIlB4Bwe>J&A3th;F8U0ioJn&IL)@jRgsp zd+lpIxn!(}=y_~H06kNxiY8otgfZW_32YDm60rf59=Vi2X45XQ=ytJTNo=Z1o*x5X zZO%f+0Oo66wNwv{VCb%JLSnrQ)|zwa`3yF+PW>=?DGO$UEjd=>L{aA}z&10lqlw;rvDYWbWDE!S z4lkPy`ns2!1D4!sIRYbW>nmZ`UNw0h`;;&Lp1orkklB~MfMt(l`#ARG2U1h7E!$j% zogL3R>&tV_JN57bcd@_xy|YiTzuc49w_G>J{_?~3-169ibNxxmlh_L$zWc?eC-3l@ zN*dK&8YTs^j7v%01Y{O4u$wFl^71Bv7?fgD%Y@kYZ(330)RpD>l2<*7BOzU|M9Hgy z=tvHVBolGa@sru5nmWfOn#9s>#xZkAjR00Vtine15IdL=&1=C!Gcw0jYz?dxxNM$9 zkyk|`pGi~@PNsGN%&koLNQ(s+M^qtG62oG}U~~+QB%muLjT{Um+D-Hnagd#OoJ!4` z3{%%2M!3<>@)c)hxVS9#Y(H7`*`Km>w7evoG0amPr#4cN1jcjLbW9)$v-3-a89YM7 zpvPLns95L_=^~9)MG}t(PJncM(sk-Vi?vo?-8$2Ap0;Dy$z?nw7=BYhNUbTVv~vxG z*CdMxmsP>ZRHw9PR&9squ?jcYC~~*W$iO!HJ4AT|S}hlyt!+bx7OAe_@ZA=$MY6EC zsJ-EcB5qdNu32N9Hl^Jvml-4r+Uu8H)&?A>BW zg)ecF%b0C)854Ywz$TPf38^Au3q``H#hJ%hr}E^t0@MSw3rDoOkrN#oe!2t-ou`68 zny{#-xiueV#$zjbN@H+}C=^0V%c;C7 z;?|^J^vGnqM%0wrMRkcNdYPTexk*I!r2` zJwprN+<4P9K(QxmVdUjkczQF|Qs<#Vl2bnI~ndHhfmD?sTLAwdh5+jKrLQI&UR7?zO7|h7LcIS=d?v+^u z*FL{O9&J|dZqd#qD&M=Rq~x)bPE!T0h;6N{6h6}<1?VV|q5@RHNpd@r$?po`j7AqH#3$tO3 z`>tGd`JxJgWn5VzU@YY+uXQ&9W z=*R%}H$bsx61o?k_YU$C^B-}0O9GtXWgE@cUU}KQciyfj2n?)+I-Q1*WjvM~9_;;A z3zV24GW-&XeOsmI@^nqdT-YS^m;p4>oFd{+Nc>^9_!l`UT4GA9>Y3d!(3c9jGTte+ z@G>^1aHYEhGWZ$qG{dlkF}p<&TgEfEhp{sif1yal5-l9W49GS_6-n+3U5H8%<6+*o zBHB=PnYgXgo-zatEW^lnTtu##pZR;>D;wg>ld{T@+4+(RQKbb@hAtVWdF_6>x7uq6 zMI{VH!&0FMQGe5E9rZmOyFTnx-*&3EW5cX;iOChDqw4UORneJAGbM$T0c2arKK#|| z%s=}n2IG}AhUwdy$2}_>a2DZ{kA1<*{wL*;>>vN&ad_GD*uVG#e@0yL|D=An;MPBU z=FYpXJKHPFQ1c%=HP_~{J!Nk$`}wjTOZlsm{4QI($Ftdr`k$27lkFV?#bEO8Tff_; zGaT(LW{??J4k`QmmyFHjWA<4OUOwT-ZBICA$Kwv(_ORJoz{^YgpOh_Tcnv5sBb#^J z95y+Eu_h){Dr9F<##{-|!K%=o0UWF|t-yor3xl6FK-;6iX8chF#WThPx@T^ZT|F@TqtdpsRml4g*-a3?Lm2uMS1}V6W%nX}} zrawXqzog+o2WCwleTmv8I3DPjg{C&<`8qCRxbN6ArQRK8El})+vu}Kvju}amBbiXM zwTGM7mKr>-}&Zc7o6<_*N&H- zf7T`6JJa#<3(s+S(RaV~y|cbye;Ho3xqR~HkNfsFPW;AKPB{OZQz2zfW#4wgB@f(r zqb=r#?!MLczWhwe7IPrW=M~a}R1@AQ%Hm0{;DW^u;Rz(4E*kSibxB34QOt3sV^f|}6RT9IA~T&P z&?0)ZQql;iqKr~gnHdcIaj7+W1HMOXqT0&$-Lo`SD z%Mn5A{y8mTQfE+cgvh$rYx~0~e$i;J+3OhIM#RPuPk`9p=`T5a6futQ!ez)XRoLco z^D!cWj~fdvKn+1VP8M3*i<QRvl+M75cec`8!P07WV? zrYjv$6-J^HMGNgJD#gYrbVL$bPm&|Ax}+{MFgW7WCCI_xSOhu}rHFYN-H{v>^H;?} zK`A*nspfm7PBCC6Gx<6my&0gCd@fUmt%z}9L_kHIUx(4e<_IC8OBF;zu~`f%VHjOF z$q{XtqDml6Dl`h)XiQPeI!TsTa-ish?zfoW{N}7iL|03H^SvpvGq2q#B7u(>xPzjP zfe9{5m#wa7l5j`bq^W;uHD)u|c!=3rNQMd>6+AQ{;vrv57|ehgPDYP%L^lK({n+UW zGGm%y-NGV@Biz^jJ&hKI^)R`)3DIpOdO0ovjzxgMgck?V^BSz=lgoBktQajFrB;=W z^0r_AG_T5Hu(nj!Ltlm(LxE^WaIr;*`_kr6O=iA!OTqrK?_~PUn5deO;bKp@aL`q3 zln`DPntXZJjW`TKXKJ=|G1nVHz4T`$a{-^MJb(1kXFvUk z*DbU3dscbJ^2;u>|2`u65wPvpp`0!ETYIFI?kw6z!su2&M>dUjQ|tn6tC~ zfFfK}@P!sJxzCe4w_JbM-M9OTlTT8MRgZKE4)p32-Pfx7EizF;q~j%=ks3nBa5G1* z;DV&dv-MjIx_Nw!jp52HR@i(L2j*Q_&Z*t^C_u?pPys zY*pXtU0$-_w=LMJ-p08MitRbD-|0PWWSK4n!8*Y$We>C%@!X|1^ts(+T!L*Mx|hG$ zsAh?x0En-Uq1^43&dt}&x%hm4h(=)&J9&ZAsi%%S(hPXR5%5KN;R_EsxP)xi0-?;) zYLJWe!j{1>m(0K&=qUWsvY74Y^_;Vd#Rwqgjz%XdMi-jvuCb^Q#(1KAHIod$OACz> z3C0Ct8D{*|ZD>@A!m7J&zg3@<4$p(Q++d;&R` zc^IVViOnQh7+h>A{qtY^97U|+iGTVrFNQS2RoBYVaaNSBgvgUAF<6qJXfR|3&gwcP zli3`pKj6$q$I=J|X0cL{!Q;7c6fH|2HhF1Ory89=MG>8*YnU-C!4{_?D%53|R@RMm z9_BekCx;59*(O6cA$j4L#!3}Q#Hv^_urDzgKn$MEt%e%Ocp~|46pRck2b@7>?^6Hb z=iWwf``Q9#@;Vt-RNa z^>^Q4-90|E_Fg-#yVr--nz_}xXKcRm&ReXq_jYR^wDbE8-Sz$Z@A%$*KIFeqlfOsV zV%}@pRS%uD{tmifWsx zW?>6atL{K0X>g(hJCo^|Ns3^BJ{d}gS5PepOxf-&zwOO$WQkKepuPFgtHBIrCzF6v z7?~n=y47q}wUTF~MKw7lcXV3Jq5IC>?d@-Qi-kACP%vYVJe6&taYMjL+v>v9Y_*VW z@Ul1%2+!i=a*n8LpUd)z7PZ_o0HRrfpMw%PHsj3Bykmf9!1dg>1_0sXvE@w;O9a!V zbC$4;@w{;AhPGSimOwXLdC_@ie*NoT{{3%#?Id6sG?uQo=sUh~?FcVJ)1hV=klFXK z&phpO-d{f9_(SY3U-;b#SoXayzm&T9+KcYI<*M7Rzx2*qu7Q*n&bx2k{gZzrSu8c$ z8{|y~trm>@lKe6w$S|Zqo`$}m4P*1Uk0Jw@Q&( zftxGgBY71WX>mf1l?hHs6)8_Sn-@4Gx-4(BDVuRty(lR{*A}FyB3b-!_MiXf-?=mb zs+Pw(hUGCFXW<7PS}mhk;CU2*<5qX75XgOf$%RQsVx3ZNLA!%Dak4#G6s>S;>mFl6B%|Q%Fr@mrIrhg zU-0bI*zy`1*)Dc>+bo;GVHaATtmls5U0njT8Uu`AHjDjTT@@Bu@ou~AmPA<(Q*HZ3 zv|WrwetBL<*4v&8F*r9yJKX18y+WJvT}-0m7d;g5=YoI;lp;ySBsEz@Pu-^?4G%AzGK>q- zxBwApDXECTDjGwo9SNj}kP&L9YSDI(8-yCy)42htUmm z8x*aHfoe%D>c}UM<>ZTiD)QNYS!o8K2ZTu$91KcT%t95(#~`tRZM5(a8!a-}XeTOD zWi*F8Vy!?d`^iz4E-EroMb+_lDwAHd@M}uYMl!XjThr)urU{b{G}N`vB<5v<>XM*2 zk|v>RSv+Kx7c^HvKDtE%?&(n`P~8s?EsoOWn{Mi2_hvzyj>J|f4*EPZG?30F|JqkC z1sl4D2Jx}n#0~9Iil>n3r41Tx9B~%0%TOc*inSu6-N~n@W+I2Pd^f%3zIY4B$uhywQmw1;Waq;Jd2Ce>}dGsfeA0>k{V)IAhMm#Yf*CgUi6POm9?(_P6<7f`2)|Rm*Ecwh02^M3JvEXs;qbmni9?*fa-?5Ei@)gm%jUKGhJ2 zj({iC5U6%>iXmqCsz9;~a5^0zPY2HOR^362R`pIT(M1C=w6KR3WvuNX8W}K_FA`4y z!|-x91X9F7p1Lz20gTx6A8?o=I8mOpbMLtIYG2*C=5l|CM$vyl(N{HG`uX6No7=#!PcpY+>b%gC{34AO-UNbc-(!`d-a?&V#YSz zyTqao7m}Pb8l_638ylCH%nOEh%Lzk$$(SR-%P=x5Y%AFt%94F#`2mnYX2AKsPJjID zbI&~nFTeQWlRy69!XG?8*=zP}Hn9BUyt@`YddKrm+~?b0@Uk7}Cmy=>;d`%~|KPO` z+;y4E9XFkO>$PVcxW}g3e_)k8c3e-Iwe1=^Z?!70y!*DR@45Y&(t$HJ`0yT^eRTE~ zhwo;8dBekZ|MjP^_xRwu_TGNAeRo`K?+>kdz|LzNzQ=nHnYGSd+pY4^-QIiHj5Uwk z{k@+(Vw;nWopJo3A3R{@dw1P#jdfO8CcNysUeob1uxa)**I4!WX^Z*7E~-gKLNUwO z=rpqcq4qk#K!{I*h~Yjhg zFw=U>-QX}d)sknrD6&UpPc~dY2-rxM4Iryl(o8^>ncLlFe4Ws7r=+ycqkz#VB5qC3w-;AQ_`{_w%O!pj$Y z=W7?8`}NB%{I>r|`8f96n=kjhuiJ0D9A2LPz#U#*_Q@|3u9XzBBz|PK56A+S)vFXr|3Bb4e*SL z&6^`8ETN_4jUzLd&8h4pnFMku&0p0y%PCk`U$wNe`~w?x533;k$}KPb#N_}~$}pP{ zT1@3KDlJSoL#aJJBOdyS9Wkt~>98Wy)*oA9p9UpVLhjaq@jG+AI-9SM#oC5oE#J-ZpMN~0Lz~j90(ua$bJpj0X05W9K zi%sMi<9?uZwP;Y`)NYq$k_KYGRx?7nRP_XrEbJ`glpZ2VAsuF5W`Qi$FfA z#Um97bV)LRF{T^SCA@3|P*+!%3<9(mB3ve_5T%n-qzWzSnw1z|oEa~MH5o13*I2yf zCLYk+%>hd(E{LAGq`{%)ERz53Z~l^s%u|eU)Cz_dhyN-4j%sf-~JV)Y!F*FqAeTT-BQ;L zd_gvZny)pw$z3fA1Bly0CZ>B{>jvr)Zm97vn*j2f<%(EhLudVUCLc3@+lp^kW~s@z zWOEti+oyjW5c`L}{co1UKoHby{b>;^o}$fKF%bjx0&8fx`1HjvLNU-S&oOzmtS!)2x-3+4+cf#uuxwDgq%s0Cl6i?;Y{nyv!Z zwVte%$yc3r0inVXa{vC#H*_a~J$ey4bu$!bNT@|dG(?{J-9X@}n;Y&VDDA=#ko;v5 zpbb(%nvAa>2A1;p*&(4RMf#nyTo9A6-&t^aI=$-H~u>8YkAAy&F<@xvB_~gU4-8=W@43@^cim?7ZQlR34{h@H**mO#@NOF&Fmr=_wqNIv znHvGi2k)}sfjh4cGJELSGuQ|1vhF?~T791#-gEG->m0WGdI#>Z=KeEQcLbIX*>Uv` z@3!6vhiwfnpYV|#j@)On{bp^r!D`E`@S4f~vS+Qs%dc8$d4oMfW*fx|$lmdB_Z2Dx zW8b*^vTu0(WIx5~(L&KY0@#{%O^%^j85mPtYbsc;r6ztGG@!Iu*jBa0r-|MqiwCZ> zYQ@lM(;5{#gbg86OMM^|+C@>x zydtJrfB^;m8cCXE2}?weIuYiDl>of_?XQ37lrNkBF9XN}Cih78WJCGv(b_pM#s20tWA0?R346)iG6WoHX=F`*89ID&!MYB5p}6j91lM-@eN z>MDMVJ7We_yD%!E+ZVH7&i)#URjSAnQhWq~k%Xg2RG_d;9-WGj)KZr;uN;cc>^=6d zUrevz$d-`ImLK>4`)OyqIO~C=2^XTsyPymmK4=Or2$!9+KNfv_JZj|BorCisq2q5xWi&%Jm@4KI`YK@qPkPMSdvHe1pI_! zqZen<-~|^@(xucX?M6%8QYmSYx z367eTU*xE$$Z>i+6RXd#M+Ee!>>;O6CzB!_!yRLNSarI{DW1HRF9_u0q@6(C$#6km zB@;7ID5f4WjvJ={peP9i20hUH@|st@%8Wx=;}82uD`gEO7&=mC5pM;E7-1I9mDe> zLI)9Vve8BcJy)VTT5fLFh1g#|;frT-%=KO7;$W)9WAyOShe^3xUY-weYe*!uolR}a z5_7jjj)!MZ>ajBI2A*`{i8iEHc-^wrCw7*B<$wQ|Us)l8QI@aP$ z9|HqWbYrST9tZ`N9lI$(I3Om0rBE1pc(W}_mUuE8G@}ck2AQ=BSn4RTC%ghnE`-6k?n zEWyQ2rCw@w9C#Um{@Kr7`q7UjPi2RfUwq-QmtT0y{_+o|K9~L6f_r@~`-XC5*a`uKF-De|P%)s)|`)z#0E^A9i&)yJT z{=zXkefFrGjyZ6vgZA8Hv-RHm#$}gY950)Z;Zx5enP<(Uu%X!mT(y5{evzzE?Wlsi z=)kP0CwOZc8j6JeU^+X?W_)w8dCe4T#IRmf*S3pE*}YtV)N+)lp5*kO;GMWy<21fpooR0Hn-yKy)qAv?H;?>6!}Up#7YzkZ77@gOsTM~ zS&vw3cmQ_W%4R^EHmA(3Bm>l3;2Ib3fFZs0Y`0F{y#g57IsyEL(5=HkUD9|#m%2*r z&f5*QtO9`=h)4yVWxyP_QdfofWUS2K%&e$MpKa)D^^bMqr3@>|s z`Mk5f>igKzjaOfA$IVxUmmj?A7U}*wZn8jfrIzYVOp8}vL_XCSoW@LLUe`EXi_k4v7>vPWN0IeX z>rYD>cjood=vF%+h2r371%)VKP#s0wT3VyXy9z8?^4c*T>a3d7Qe6f^LgY=dS?kz} zS}Vvn6>1YPW=lk3^>EKo=rByhz)pTCa=Gc5u0)$AIwZd=H5tPx@+C&nLJrB?X-Qc- zqDvTY7g*1k7(m=OvfHbcb+UCkAxshzbsdTI#VuBW9$ht#z5w7MOhWjZ zBI`!i8k77DM-1IP>pB%W*QTcT?Ob*;MxrPe3^Ewp1H=GOd%f2n;|7IISn+XdLKRnN z`wN6?S*I*#Bu7S6v9VkbgsyNYN`;ov98S?y!QjYdtx(%>fk~zD3TeI zT2~?-bQ0=v6gee1;jRlA7K)x;K|76@Wnec&sop`T}Go&Cxe-s;#r_*R0|smS;DZ)Xkc;7Bvz43h`EW)L=_MW@<~p3^4Tng{Hir& zcuk9CiOA@s?%0@oT~ltEii74*>Q;}5$;`zPS47n_AhC?Z$wJ-R^B5*9qI70N*9D&T zMooF9OM`gY8DvCy!iqUsC~H&rA; zFe74+cdV|y0hfVn6QHMiL(^~srR19zrXI$23_Aw|+~%T!F4HmB;AImuJWDJaG`&2i ztH@HpwTs}}Q?_O>iMw)Pe73nf*~~U~vwhJEv#{{meLqFk28L7q zdd^(G5;RR92yo4>{^5UEXVU1w?5#K7)aD?H2Vbo}E$>@Y11uJw-oC7Vsu*H*B)rxa z6WUp#R1t>xdqRrWb>T1B$g>wHC+ABE#{P@vX+?K?7@bk+IsBphaQS zli*hJSrXhMa{yteP=bHilg$iZZFpI5FxPXK1kroryXTWJfR^gYwBJ1t7QHr_fm2V7 z*lrG@vd7#O24OHXfj=xGlP@x14K65hM3+o{`k^@MHjdh{(Mg-iptvG5jbm4q)=`TD z{X`E54+6z@-kD+JTi#Jv*e0WoYOF#bqun)n-iaFr1*FjF8(m6VxZ#>B$$`{J;8!k$ zI^{ViVdD`%hqA;Jp2Au6Dk7pdhBqrED!eIEEg41HUEG+VF67PtM99vm{NRi^QHsD! z37hKJBtkHUQbk@uQCh;Bc6Cu4iJER9VzsK64cSjMen;w5$r6-=I7RFOm?glu)ai7*3`CFDnSc1hr=^!(TIhM~AHMX2 z^x|{#o?ZB$_m}-%%KOWYKX{vMWt+?J@&j|PTJXpXkKTXvUAJ8bH9zt2b<)v?e8~ID zyKc7@yu9ZQ>+HDsDqdUOXQy=!-1U8X@3_ukvo=0z?=6qsXDeIG2ko-K!80eD%U)qV zc;Sibz=QTq3Gpcs5isf7%)%_-ZiAyso*ows-cc$s2H5eovO5pt zpo{jh%Pb=iVk)$u>~ZU}PCMBSGQ8ZjvgfhkWyy{+1T87D%M4Td*w-m1eSF_Nw)ynO z4w`e`H@w2^dtV^)6?4w=p=|Fj`{bAJeMv!PQ@IJwi(CJg0=i6OObUpS z1&P$e-PL6JFg29o8ON?QM-n8Q5>cHlepJ+C)D=1^GEY+aTT(o!mX~2C*MzZ@n^sMV ztiBL6ac`q*FYRIlWv!)drOb0G;j|vp)%6EOY-ph(U#W#C2CI||<^mzKW3ZA@YO!kd z(((xfCu0b~lPC?L6g`0!((>$7D#*qz%|xu$VL!0uGITe|9+Qr(Us=Z2j)Yj5*Fb6w z-$|oa5sFf$k~|f}N?D7-01k52+S}UI)!o`y-(ZZGv@+*ftAhGK?WGitA_9qG3^`mA z0Y`aIOAP74NlVV+Y`M!q*R@)Tn1{zTuGErP9uLXlhK-iQ3I$!RoekGt-|o-8vu6{| zLOfhkH^&VQ=v1R~2j=yaY(HDK-Ug7hI|(FU3g*~2{a5_&~=GKP;iLcTnC{9E4Z(|=$&n%yw?pxehN86I(zMHXbTXMu zpp3*3M4a?WG7^)B>h=OVQLqUS70i&wP>PO$I`UH_4ww^C!9gY0*dEF0dv z^2!z)|L5QT*G!0hN+dp%XwLu_A#bL6nGclqa93j>4XNerZREb`Mv> zT;vz$accsFSojn!b5Fx5OiCj~@EWBkG-|0|{PJf&eH8<1A+?&!B3y7<;kPEYIEM=a z4J?HU#YC@#7Gb&xb~cRs!lViCR#`0 zpcWA^v2x**CavR@`FrmYaKRU3riuXJVvgvvga||Fv4gTmhU57z6yY*eP{btg6%s5& z1x>ydmnrO`Dp2xNOG&7Q4EavuS1J*k7WRm?M7w;Yjy26V4l_`w8!39mQ=Jf}!(dvH z=Ew%8(=ExDc;q|HazcugjJl&p#;N$Lg@GlhORT(1-pLozvyAA*J!kQF8jTpZX1-kAk{uJ^AF<*l4z= z-1FEd!18#h*^}5m_`wtKvh*XL$o2~JvyVP8|897B;rzQFyz{z8?wLG>?ak%K?!O6Y ze(JGXJdAzIb?116dBH>1E`0QcqYmDFtBu~~V_%cB{aTXOmVNTeBiYiCdw$@!{kQqp z0o#6LugwqJ?fu?g_8n}H`G6T~&)V{xv$k5v_r84Q3tqOr{PBY~2bMp!_xnG#-zGl! z_0hfFci0~9+v`JXY`57vcG`0Fw=6%|UtR<+185+m_w~FlY~=&G+PwmzEf-BgHdB1G z6B@+^GFutSK%k(m(~hQc)lJ9tRh3$(107JLHwOVjKoWX2ZGqC3m+&vK*7jf{fJ~7D z6{><_F^uTLo3?U*W%jVj3r7^8+Z_hCVNy4lJ)CS6XUo-XXW$xxDG$;&$Ej!+SUyWs z?13vF7p|ws##}Jg`oR;?Fg35)Z^PHy$M~&VteyTn=N@ckx$1Hm+eyGFj`7ve? zqAu3}VR#u9mhtj(co|X#nfuHax@WfSGUM?9?JF-n%qHjdib8(Bp>_oJhqjFwNSIIE5dYa9bLk*qW1LUb#`76v2!9M9`?5y%oKgYN_~ zEV0-mF+|B_R)wAb2d2WOMr=3~15Tl!H^aEkcXDL(-psc8)qHfZ6F^|Iii|AU+%zsR z$&peY&{k^b5F$$*Y&6PX4lr^juwZdcJG$6H+%8oQ(y)zTRLGD+so{=Z&N7lanIu!! zRb8GOJerlAY@n1=TyUga&`>y7Oo&Pv2YJeJ46G7=0e8e%%teVMgiA6yhNB{?)#7;V zMMYN@A{BWm;DoacQK{BMXT0hvBJz$RMKa_I0tUmB)?_w{C^07OI7L721UHH)MMe@>7*iBKGw`F66Idoq(-4JCY^CI|N@9;9L69}RkzU)Y1h#Dvnx?!aaMfDkqBW` z?j)AMC2i+Ic2Cp<%In+_aNVe%qH(!5^O>oxI#r*(s$J&m+LLqdLj93*7M{?BS z1ygBZr;JltazO?~qExB)EHNp{cWi?n1@kBhY854UNnVOhU9P2(YvM-Qh$=Z!{8h=4 zDq@zXR#Bh^8wt}HoJzxmQA#qII5VCNc(RZe0+KHn&U2TnZl|L#h8eIwyG#v|+N8`Y1`@i^Ye4AKVLIs2c z`67DkydNvu=39G>H2`h52zUWagY6bAsUy(ca{TD_on>tC6Jklz8=By@Q;RWlrIL_Z zoClWsz}L;!T+&uDjO+nyMLwNv-x*StdVl%qOV9KRW}C}U^Tppg?OUh*zRhLtFJF84 zxf95I&78S6U2)f~*WUB}>p^CCS$wQRnp+M2El@2p%}MqHyjGDM zml9A$IDrL+8CK&h=dcM2dJa;A(;Af=i!l|gliA$DuMlAtYSPrTC~XDE3vx=?&r}uV z0Ri_4Km+#IVI29zYg({5s;HE~w2LQoEO9vyTeI>`FvSovrwFWd5E;P7POY4ChmQr{ zDYMCB27?jVKY#v14EWFg_!nNQLMeVLf1QD9MVJYc$1n1p);0jTGv*Oc548A^7Q>V( z;v?G_=9Di~kgpb*JYp(G6gd)@Q>?{CbG#x38S+Ju8F{zE-&II@t4Yy#a6x2?|$zkVh&Aylib)cV#A zpfFkYLd1Xd=f6a;F16;g{`6oiq9;@!<(8~8swLp$v&5+*y?O}YT*#g*k%vZsM@f8y zB0%aWP>O=CK$9e5uo7R2GYO<|hY-4W=TwrRG5SQK3(Xxr9l(Tc#rw)L-bn^a+hWF` zrR>{ki$C)vx2X-^8Y?3DNQ8_OP7#gV7ZVL5M3(f9^5F$guKiP9NKkO?&PQTN(>LT1 z!DPlrIthbO1V`Usuv5n}zx>mmtHoxV!V(V&nugv$E-0bls1BPThDkB7Hux|M&RUL0 zDl(fW_Nzq#Il!`SSzFxd8?~&+>r2K2m#Td6*ZR`Ps~9*UKjAV2lICMy6gl;{xHY9m zvJJ}9@p5@_(1N_?rnm`$OmUdRaLN~hi11RxX1BMP%|VIA)D;Bk_`Adu+Tyjk87xDZ zD$`rmDVc??7?v++8eO} zCOIk+a7hL|bv319Mi!h=H%nqhEKsUr!|%v`r;Ln6Hh$o&1GQu>8Brk{@F)uG5g-vlaJl= zlNT5K@cH>Z_Vw~J4?p|Zo&8J-WPX0(+=cURzVD7peDw=ne(Zs(k3DS1UA9@{fZaFQ zVT*Tfzv(;np7GxOcUj*jvf<^A?zg4Se8J1d?)O2*kIeqSG5c+G+yPr1y|2CI4M1jK z`GB3*^d$CvJFVua>|+nu^mh;0>hni#|9c%Z*{ zOW9xc*NIn6K9T)vylmkIY}%Q!l?-44uz(@ZY83=~Is&$m6%nwB84m@&l0XYi8>TqZ0`O9S2OThkh;33^XyL~o1GnObvdP5EFj&=F%vNP~Y@vAH zw}k8AR)dmKTY&(ty3D49M*T=f8Y<&|98@As%EoUG*CV)AEZAr< z08*{4N3yM9yshkxf#p3;8vC`Y$HMV@oZG!Gf42NZiM`}P~Jw7(1&JGGna(_Zpk zWcG<{880ux%dqoz&N%6`Q$BO_5qtUt^I4~V;map}?5xwj2rplK>343w=`x$kp2xm# z?u`%Jd9z2d9j)Gt_J(~EdaFt{SY5P6YHIF!F$*}g(vWc-xpth!X1l9Iu>J(`jz1nW3 zQZ|s^>blhwArYxgED08ecv=fNTA5f=p#(Zeu$EI%6`bVYQHvbO${8mfPH}-5*ifvx z)yhJ(B(>BvBP*ps(j01OCPx*M;BiDfL~@J9)zRusT@nby+``@|rFn-E4k9^wGO|E1 zvQT8OBjZP^$uMw`JuS&tA|sxx%7y$L?y!U+Ruz1D6)iETC?F%zPB>kTN#g=*2@JFF zC9#N@Tg($Oy$S}}EpITZE){6vxIGWf@lzu%%Y8f zC@I&dE*g&VI2DmVyM)4>%)?I+EfOJuZ*X?u7-q#v5p|B)EMi3?Me53^g@{Lrl1D{@ z(k#zTa#F1{3&p4ETq8jwS%S^o6-A>Tq7Y7(S{hw-6fU5UpG?O@kvm z5~7MuiAug>M06QEyjF!wses@oDhi%qs=88;2ebi;z!?xMBq+b+H03EpW9TE3ig9IS zUeRz=Dy6+621&P+$jJ=HWD1CT1$vH#5FbCUbZNV{4?Dx)+~gG1N(mRYo+7qlZ^3Nw z**X)YwP>d;P!We=;vfz4%CLa}jG;&=jiM1?l=-8d{X`NLfXGwO@`_-hQgDg02oOU?6?sU+Q64|G0o-`u6j;*}Jk(teX~?Rl$?GRtQ{=C4wj zJcZq4V5KD{mY{1*Jd(_GRHby0L^3ZV@<&~@s1`1z5edP7t?*))E=Os>LClLvB88YzYyC`;I7iG3-|nMJkhI4dJwB>{VwldOQM1j>M7heqs}tE)Ag;&gPn9_$0BStDc**GKV*iK<&U>O#+ckI(%US0-}eIFZL-u|+kWkvEH z+ji=;Wlv)J-j@fnee4Tf_VeY(AH8$l1Gjm9`6n;T|MBzlo?meH!biUEt6wiYb^p(Q zGVhrMxAlE&AIJ7TsU!B=+WX6g?!AToNzK^u-9C}+XHxL;tPj4&Tg?0IupY?lVeH9M z*#~ZO^uAjjvB&!lp1HpFmnAPRAG+Imkh0BXe_{UcUK`k4{`7&HAAi7RAK!n|qxatE zklohzJoZjoz54_2eM|2z+ZO5lW&6wCUxsrfKQ^*G3dh+m24?MnMh8WqOE9)mkk*c^ zBOYK7T`8an$-(-BV$nF*YMxTM~i+X8>768Eh&Nr3e^@PoZ)YlFeDvL7<4M zykjLM5lA?w3%Yt#n_kN^{HlYWq*<_GCR0YKB4R1B6%PD*wi#{(KNVp>v4?J_yH}M# zSEzsYU3bN7r5oM;YY|K8kvE)ia1;OI3|#^ zVcW14QLU^(EICCqg=N3^Tlw?dSV5ZgttDBjGV1WeKZ4*?l*C6V3muIxh8I;VjIj-q zaAHGm(XL2BR0SmiI>p9}Vs0TyjX?K`MYd%Obr?$3l^-^%SoE|YuQ5v~qm`=?Jw>C5u_atai>OYT z(j}(Rj@U}c(aLnmkBBjzcdir^MHzrWEzY8pQ0Qe6H^o6|oQ_G(ydoB7l2u`hfZP5f z$}5dV4|lY@h(W1VXf{b82g#>*oRTk}!Ai+@%yD9eo8n10z3OTa9$|A@qtpaW$;ln| zs7taWQ&?n1uS+Rr=INPns$Rlb>Pkc;oK=paq71!KbdE5o-ZElBow`mL%yK+ZRhUHY z5STj5#3Bv@qEJCNXQ$gRm=Vb=u-Hf-JeReRQc6Rk@?6F#VUzGnM4=B|q#-(DKrd)R zCW@4W7=)mrvV9nxOeI?Eh8S) zY^&A$=kcv8zR7J;X_J2+)(xF{6K=#vIdbY`HarPUath}fEpK=%fr=_=+6 zHnrqQka(>+Ei%~TV~e73;k&9ABJ_AV?MPjTYRSSrq#U9N8(R6xEICb0A0()oRUKJE ze)OC|$y#QVP)2HSj&s;Ul(1+Hqo~ODi51@}F%gJ%SB-$+Z&J|Emd#eD)|OVJmZK$$ zPixUA@)Ek!$?A7%QkZUec@~n!U?y>-JZ1=}n23*ER7nO%1eO&ERDjVDN}3?wY2Vf+ zlW+oygQlrhyoo} z=JEw?KpSCcsgJqcQYIzGk%~@b+B~g@?@no9EsZ)-3_-|{a!33l(IsiG5`a)*)v%|c zIF%0b7%7j9FIrPkemUmUWDob10Zj|m>tysOO;AxyP;5+=XNK3M@;j) zgVNO^5Hr=Dg|;sY{DQ`A_M!u-z^o(5D&ipurUrEhcZ%qg4Hy&=2=m%FMocVNofkCH zf*&1jg@W5mcZ7LW=LN)}6mnM`Qnp`!h=Q3%w$6cB&rv&dLjVsfZ&#WG*21=ZotQcm zeMJogma%aR=>)wq67&uT+lXg5pu}>x-n(qth$xb|053bWndKG)V{WY1l~?${$|pLoKNXP)-?uYU29p2zkuHoSc6^%viD z>(xGyeaFpL`!crom;F_W%~qZ*>RNWXI$R^B0HeLU>A(~)rU^Xgh?0d|%QtFyfX`X>3#7!j6C-^Rb@5{+s_*wVrP7>ivQe8zdQ$b*%aEiz<802fdDt1%_oi0U&R!7y7kVoVo{-&ffI|)%sUaE&ELSfHvL8qmT zSUiZS#-r}2-f0d_w+CHPMI3~V*MlOjItxJq*hj#PIW9(zoq!wOcFYkBnM6oIV@8S! znp1L=vJC|@N1A~0NL~h;U}|*tF)&9_F;OyA)Oqlyk*P8xo~o2khHcF2bUG!Q$xMDK zvXGIfMwF;vn4>)Acx3RHRl6e{zalXd0i3E(fvxI{l#+ue6D8shOF|maRWHdSva0AI z=A(=_s4F0fjK(fK+TE6{OH)mtl$KhPD3nBD5EDYOhgkW-TdXjoOJ;hWz+r=oDvU|B zN6LE-T_b{}8WgiDQP*;$7n=-vGVL!L^UVdmP|-dnIyRPgHOX%mJYpoT2dc<&{u0Q% zVL-;PK|;i=T`=H>$NB5(=mJrMfdp5s4s3*>NcAe};e}Ln33FOx(kPPz{My}68Jk+t zbdy4bndDf-N!=+(*b%4+0K!27;4G3MbJ#$pwH%LxLOvrMZLg45cv6|BW@E8(%AoxVm*?=W>NM@5e` zDp07)3&ap8_#+bn6%d$cAVm3sX4?^H(y6pBIT9mD;~KM_YO-354dg53NMJK9B+H;s zlokLPfSrgn3VBI<{zK3xjTn5A%R|m#ZB^tm&#~70_3@Qwp2N!mHQ<1ZMgC+>J_TEP ztQ0nf{)o+ALikRjl&(TC%uC&fhMp5%#voxHNt7fSO92o^21sCnb_{fY5`hkl%ixS0 z_=4GiSyiB?Dq>Tek3!m*Ce#{Kl5u@$0!K+{ts^rTd7fEVK)|&2^$0{HD&Y{EJ?PtR zy8gy%uW}R|cPcn#b6M*+?Nx3W-id=Kb@>RL2r#>po5Sn8s8JAycN|$_Dp1(;>bf_9 zL)zk*giH^022>Y6h8!0+nE>zfYp2UcX%%Ir=W*&VQkHj#jh3peqf~YLG8A#SW>o~n z4!o?C{DGH=%}6ryPKM2R2+6xlQc8Gi>FS7%s1cp|f`Lb-Wz;2^*=&e`JzBhIU}hBt z6>-+hFi0p;dL7jz4JXx^iXu~9UCdE13?9Tz(?UWv5E#Ex`7{#Zv;nH|%D7=LDK(bL z*Td`8h#;^8BimdqIfb1a`vREVXZy=el{!B2%sk)6{^?I&@K>q6_w}O}7x?6t=doXU z_EG;|e)jQu;AM|w`{b9WvY(uHlP9sIXP@}~!*^fy;GLKFKK2JUUdj8*K9OyIdFBUK z^Sv)$`tlaD_m_Pc+mqP9vhQQV%TteJZ{d+_-~00Z@_sw5zVD8!_2=bKvyXj!e4mYd z?<>4~=xB6zv)V}oGusn2u4%TODrXa^belR!}v41g^%wuo5H+M`u`Za6X;Eg>fZMP(TIXz01=fz=CPSU zkeO!aZe|1oK?XqultGQe2_q_^f+z?oGRi3P4BZIGC{Cy`CQ)M&HAZ8Ixk*mWUF+Q8 z+|T>}(sf?D+q=$Md)3;tYuDbpcGdGd^?RS;g_i|DpAeK5HrHURlBORD7|{w53oP$U zk-O{d=Y<`%+t$N3Bh%#mRF=mmcHDk@ zA0g)p84l3xMRFS_+b17k1K&I~*SFN%XmID3cNiHc29x``7ns~_Wx`%zcB2_mc4rxm zCb{VR4}Rh!XC8Od+;_h1h>w2g^behN@`a!Lkms@C<(s~8DX@IYb(h_8-R1D|BM;o+ zFPH_%X{e#SnX|pa$F{1f3xR_Lbw#ixb5ZEhoJ6VGwplONh)8#Yw^3}nSVtuzdbQ8d zS1&s^TTQ?rU)di{8KF$57k*gI;80Vgph~mr8cE6+azckcroDuiNt`8w*UnaVn!?k> z5j(2WG>p*9h>o_Vb=NWbMub|S4C+331J|Vc$St+{G8N^{AwaFh zqu;|^QbDEaKe;D8tcW4*|H&%Jl}FDKX;G}9||Moq_Q zu$(D@Q40nJ0BywsDZr5wl_fiTLU%qRG5NH^w#aGC83PTPjGxn^{R#b#AeikSW$g zNOLmX-~X=1t=(Mq`E>8O^C)92r+~A^yyxF{cdsztb=%EuD;rL8jgbVCZm_U(1MgP_ zQ(zJ)FttQ7s74Vil7!Z6Ia$0cGfmbs+G!*z@$tL7-o~ERy=07#?TB%79^emYn0(xRXTa(h;o5tZMrX>K4l$~XI7Qa zBx0;>YhDbDAVw^e_+}^L_<%nkK zI2-J+?_7bLE70^gGhXQ=W-+&0}*~cf4`MX}Ue`Mh0ryse?li2Pr_layU*(b8mx1YLW!QEd5nc?N_HeJcX*xq9H zXHt7?_Zm-PyT1%FdmbB1c7M6M%s%#Y;-Nd7a^%jZ9kcr(yRYN_mk-!w_5F5w^#QxQ z#w*Oe`gP=-4Nf{>7Rc=7Wsv#!12#Ks?+twK>-Af{Vyg{azQT)t@BVUl83Y3%eJ0wS zWq=esgy!5t1|%UwU=U^mPQg708mt2|>42PwCX@ydJWkQM_-Xc z+zf8L#U@_6E{%qd;hJUPd9ahjeuFS+F_>b6q0fE_ zc*G-W6fB5#I!?+)J#;Xmmo6hA;=xl!#SDq>KulrpO7|5diOa5*YwtWDLr17^-^com_1CvNkruT~M;w zV3|sdDH2UN#k0evuz9C5JTjNNu1VCZv@Kwhm zY2b?}5DLKX^~I!*K_m(JEnU3iLSosmW~^STQRKq~Wt3~eX?c-rLb6d3I8v?Gv;y9^ z8k(r+I9gS?b`(gLdxgnqY86#Oy`+UE5c0k^oy{(ag3YM;)L%qW64ImGP2!JC7|LwV7yNLM6hTmsg2pBO& z@`4No4#=!EqR<>MA}y+dPuaNWa4H5QHYIoT5vIh_r&i|2tZ~g3WT$Lt(I=$L1v|_Z zSQ2HK$rK$Vr0|Zdz8(G``+rzzi4;Q5qwp||CF4qt$OWTGJkt`@lo=VLYmCDV1!HH# zdlvpP&f=lKh-foa#^^jRSqQA!w6_awL)%6aAE{0xGmB@sECALlNGn9@Bw1!K6MhQ) zh5M2yt4dn;SXIF&eHmn5{+j|}vo>Z>Q*)=J8D8#QB%6as22c&1xw(9=e;k&Yk(HGaw*8@?ftHIJn(rpFgO~RO+QK?`Ge)m}Ml)j>#|kBevF$ zswOnTs5)KJK_CMZgPNmaFl)_GO~H+LVNZ9NRseaRp@v2$1U>RB?G_c1WI2%sco`#6n8uEg6A3gy zm;#*v$Sk-QY&s~Qpl)_Do<6xQEe{wo`y`@57+nZ;QR;#z zU<<~a>PUi0MKR-r-Go3@GtDV8DYU{jDVF!(SFdJCLbc>lBMyAs@!SJ5Pc7ORV{^A**$bS0C z2a!Kte(JINzWKxhp2z<76ZgBn?B!)(xmTDykL?!oV-H-wI@c8eKa?k}Y#sClP#58QdZgLmEF4SRTJc~k!<_2&JzJnq1)y}axe^Fh0< zx&O|uIdIq2-C_oo-+aKPzK`wxvj1Q9e^STH-RPj**WP2BRec|O%k@`$*&qKNUS4LY z<-EcSsUfHhM()lsfC-s~G~qs^3e7=zkS0J1W@=UH2=|3{=|H#uBajGP`V=-W=m&pj zG-WOj)X4(Eu$(WJVNX13N@}bDq@KqHpBZ73gq(ewRrr^8-NmCXcikF>TLl9_vqOOk z5z?XQZZAknQ%R!Lc**Mm5vRt5NVIaKFKaB*97J|0&Fo3%7H-eO+*=Z8M=OGP!F#!l+^#HC?jtUA1dUQ|llX_l`1} zc!yyfDe%?Bg$2A#CbHkAHo9YmU-ChO5s!pJXGBX_U|h58onxB}vty60Pp|abn5ow0<6`@faB?AsuO9O;di!T{hcIk<;-bhWkzStJaB61NEgi zKdo3W|Ih#UFHLrOzJCj11IxPe&YFqSmrd>nnQCX%R?ViJuxZM5!z4*azp1B|LPJPF z*sL``t?4vts&pC`9i%EG#OBDJQD<=RlC|Mf7n&x>X&k>q;t`T{gc{la5zt3rH=Geb zG2~P*94SR>?(8Z{k%2HJZ`3rgLxN`$#-YgwM~$v>tZnY_5jDM%=p_c$B2Ae!5iDpL zA@K{&m`V@LntBV81sPz0rtWz9gb0&sjMmg>VrWt@I!%N49h8O2gsG8;&FmxHHIXzB z)g<}Kh1RO{g)L^;6F^;b&+M?nf~MpnS4^yBf5wguVhv!HUozi zks%OrO+mG(q0kRaf@y%f(`-1(k{}*DIsFc<%z7@GkU@kI1H$MvNXIDqZSUb4n}3`9 zK}!V7gyUFdSWZNv5U(+qgc$s(9bl|8W zI+Iy``UwjINbyh5U%KQX)#rZmDXf=*%R4EW+nO(9HV8H^vZ|FsO~REW&82FXctncE%SeDWinNkiqa^zQ_||hP+;N zr$!-`3avW9j3zv}4AU$;9!Yv&K&u&168Z&$j%NwXML@C{ z$5ZfI2NE02j2GbuKvY`w6+=yUAJ-V;;+-91K^b8O6W_Q5!%pMX8f8t)SzwS0u^H45 z(qsW)c%hl)#mk0J*wk3a$T(9?pRAl!RF#%-yhZM0Xp#2}wnwkMtsIen$w6j}CO4O# ze!`86hv8)~*^}7MKKrBxv!8tY{%4+E;Co-sJo(_0kK8?Zh57OOp85JcUR(AT%t}vX zKlAvl-+1hnP_y@!zjp5xryjqr?|mJ(*QOrH-hcNEyuS=A`$RT!iy2HlY|l*&pS|gX z_S)pI+5Sdq{X=$N@0k5&P5x49&$SQSbqzO{kJx8DVEHYFZs}uRUSW2B+5buTKK8+T ztTTJN)ppo?s9as<$TJ~}WR;1<-y zkN~#8BUFe$MGBxF{UEcCMW6Ma)A711A>6Pq9%f`muV7{Y@I}HPQ@|#n?~l1d%^>jT z&ELFm0l=mfEX9*RngA)wyy(ImA)X{5d`f{3@NZ~b2nNwd5V5F)WcW(G*w?EPgLn}e zCTNe*OJ8GzVv-vOh8kigA!~TO!~hNxstQu@Mg+@~7$(umRzw+ga-9heoY)sE%R|S?mk3D%X`%~|Sm)&26m(M%rJzin{{AbQVUSalq z?3s^!UF~~cKK2DKyT9ztNP8!zcRRiIcz_$;71{aUF`@}|MNqb{a4vB; z)e^G7w&n8-b~EMP7T_HI(wDNnnC7J_zs*r06&@W7qJxS}IW1Qe7!ro4I)Cb7;9&$! zlF4|E(xHaf8JWq2z>;fPoC-VdN>x*g3Q}1Y^2oaNOO$sa$nB*G3o>7$S3@X`3B_DC zbQxEHA*7!#*9+Te06`2=O-`aqNf^_eJ0i}f4j0k!2Fo;=Wk&u#r&!AqymIo4ieY1~mL^M;QL!woc zDe)shk&vgRH3d~dtz6?-n_ef@5(^n*tzOe5WoDJSi`iUD8U+GL^4RPf-LwH5)NM2^hM(6v&W!`iiDR~nhomvQxdx=fEEn`HyfxHXKp2y8aegyNyuvBMe~6Gtry$k^)J-bZ1#QC*Z-I8ssZ zu0$O1&M65V)k;!zl<6y*xJuO#zZr4D?1wdD?~4Rx%0NoBu=Ub~O^ofdy=WV9`%+aq zTM|1`8&tc{cAgkj!g5ud63AMaVM=mHM8Y^ZezHWT@G|&=NT4AQ2^ds`3h|(mL`j`h zrz2}ci-{+1sq@l!#V~1I*pZPj7wj+#b~P8|0Z)wYjP?F3OiIBo7<77X0!EiHoSO6+co}18(%!Kz&sIniZ8lX+^jJb9c*mE7Fzbe~mKu)1~8HZlLQ<=dm zu-}j~J{=EvmNiieFXJ7>Txi9ZAb;lNd>v?2l^+;ZtEutDHZhF&bEFhrLiP)?LGfZv zSs3FYXBp{*M}?Grb{Y|dhL9SbCLu>kHj9n?f0Oiz*wF>gvIqsNoCs%5u;X;+jN$as z8KzZ@uhj~-o_)ou7n7BL^CR!9IYnFieyyDlAq9B($;UirwLtzRuPuN7Y2U|w;)(gd zvj1QH(GUE^*yH|u`Kd?m@i6vyf7$)zNAJJUZRPOtqw}x5{e}y@zxi72ID2 znGe`w!#O*z<4a#}oVz*l3iISMUvszeNcJ1|o&_(z!M{z-;AQ_Ob=d6nJ&!$i$JOS& zel`CmI z#GpxEMuxTS?GhTAu?PUs#M_5wT&V0w1)KWH8}Ed4gle$VfI>Lr|#b3q*+|L4% zMj1fnT@w(kW6=sjM{$z-4-sAPMp3A;;Y(j&GO+B=mpzZ|Ui0ObT!2yf>X+xVA?3+q z*cW~RSU&F~@AEviTg<@nd(SxTqVvzb_<|36dHJ%>pXbk)Z@e4@GkKSQt_58w@QxjF8(qpa8_IUAZNl}{gxauCb{Hz# zSt(h_h(i*S7drM5B&cJy1C|0`>}Rcx&tHsY{u?NTVk_FhBk#cPB#;_jV1;CuC}naA zGQbYMRfX!BNRC08@U|r^2$c@awrUtnOwpl`9o3AhW>8bR8gXW)s-f9S8zUnO3R_8{ zS9?PCS@y)HbCCvn%LT(iHZi&@DIko#C6gRwGv!gzL7H+c7(|jC1K~Q$ z9e3hP$UC($iQ5h*Oj5nf;XNnxdMX^y}8D&j`6cA>b7YXA!nlkQqVVoLA zdAAP5kWF&-{QziYEgl^icf6Qlc7FToU-+okipwwWD&Srxn{))vvWuYa`1uHicqn_@ zqL{TdeSDEHqROT)IMPZ|v>gt3t%9k$l0-c8%{3!PFFKZYv(Eyx?AMVCAt8(rWr`gg zAbC0US*zI7Y}?Ib=t`=26ah zDKnbnO=%Qj6TfW0K?no>AZY;-Lq^aO#592FM04gTeX%iu=SA3Nwl%CYcbZC?kq*3> zmoXj8^4PpI5j217?SL78?1nO!CK&LUToo@2MpWtOh}SWI9F}w^4(I?$?CPZ=P$qlf zf_C_$Bkw2x&Sv%*gqn@IJK9=5nqHx7!LU3rK6AHG01$zw%u915Fw-O$ z(8HVpDZ--@(Zm#X0jt(g8Awr149*6MPDr5D-Hy18ge&9kaYyS_WmXMr|yn1LKWvz`5zu1TRZP(_|8L32+$~vxWxf zA=wUINk}7VynhI1HAh)v5JNN6w#1Uc&|$y1=1bo2`WBCJ+M1>@K^WC$gc@d=&yJi= zBV1~YuF`lJ=SxSDjOZw(Itp8ansQVaJQ8}5JmuoFh99`+PB-@%G*c8P`(Jf7HJK*8 zBvuzqWdozy$)U*UG#oZ{=QKN7b!lqqPC}DHRIkFLS?jQ-i@tuQQ#Mj1K7D-gU~;F5 zA4C|@7ckP4ahik{aBJ9N?&R5Nv-R9!)`alZvwO;Lu`hZlL1U2FJ!MZ{tAfe!vcHkS zJpQQfc+BtqvR9bD{`LD8EV$GCWqA3of9S`wC%*Ia0(kkc`L{mx@SXlf3V9^^zFV)j zrXWd^u<+y!bH)|CyFZ(|B++Ei7y)WO#K62g`NA0)eA+t9E zligqTuTq}MK5&Rw z4nLCcUN9^RBREMJE`~0_Pq%1wA%RaR=%SqMNSy0=(?w*uM8Q`9$`D`)um`>TV-CuJ10p4x&!@ zE)DG~D761{)R0hcQO4Ly+cJegG_~tc+9cTDGJ-PFHC+?xn&SErxzy_lkw4oRB4bWuxfT2484hKV!@gJ1fTkt&^Hz_3q} zM?#LoUz~Z-iXm_Kjhfj=qU498k+CeN%-!fqpH76AXMDoogDwS@kRuX-De;&{B!d_y zRMk<~O8oQPpYjKJlc&gB#O;5&pgY030CnbfJ#rDzWFtXh$VfJ_)2P|UAeN(sGl^>E z7kgV5M(|GBt{QlzY-wBxE45^~BhGzEjt z5r$2J#b_E91csQZR{IXD74MRW>C`tAQAxOuEgdsI0DMy{MN;_W(efXzFn1uwO{G+2OZSm+C+M!_P2M z$9OAWtFh~nalU$(gc?&B$2U6OF({ovqE_%MG!bP?6+?%R{dAaOghVii!Z4m?5;QWL zG98f+3YCOq21m?^`mE>Mt}2zZ~n!%pU{LM{PvSy`_5AjlYjqPk9_;7 z`QBFcHSE89_VFKm?}=xh@ej-QKK9TZN^(zSKk@Kgs^GIXnjgOF`uo0m#pCziaQWvx zboY&)d+_#)58r>*ygk-AV6S!e-uX4Vzi!ppuU})IU0&;5=7aa%;^>24@8f`EZvj!G0sX{0lVX3$;$EVwsKhhLUn2Y7U2MWtBf`{JK<(U8RIogq`Nd z54A!y%!nrAd@-)6`0>tW`ld`LQ>u(-t#UdN682d$(PJJbv;X*`_5;ZF&-P5!1*?;` z)3WQAT|ql8<%C&yCy}X$wIZBu!XqA?y^*swvrJ(avCs64P_J>>$2z5xyNOX!3DtFF9)bDcxjhq>tM1%Obt!B%Z)SyU}8a*Xv^iYU}Y zgc9Nc;fYPVag1tBWlz8^vLY0xEcsLm$n1J#6q>rmVPs6Y=+w6v!OM&+VVLFJK$K?6 z{2B-s7_H)@Mj}oUd^&i{n7bxU%sMJ{q{(vKRU<;0ja@5#gi&AJK_?lONlv-QU^eSQ zh_b^Vh1}ss0Le8iPDP$U9Wh2h$fFV&Dc3BZ#+U-3Iie;}h?-XVctVbHs?6p{b=6Ga zDHzD0q0fR=B}Z&3SrFlLmME+0TMk=7rG;eJOu@luS?a9e63ag zx!|2;mJKlbzEnW&dFGh|5sPEiZSQo(n(I*HgoKS(fA4JztxMX|4o z`>Lv%bSZmyj=X2h1z+UUDCb?01;b8icyekCik7?Vq(Dc#$xr`Ec-MU8iVuA7tg}D-e(oYMP(U1^yp(FZA&eud zr6p{wh9fo>S}kHQiPOids-}KRm#f9gx>L{*&%P30mm;xZb!5;%oi%4F*brf87J#yjBpFmig70iVA$LrfFJ}fv zN6H3^C@9TcS>?3sF@o1hxzUvneBCvTq2cM*G~}aiatb5~nKFuTq!Vh5@XTh74$3b{ zBf=O_8Pi0FA6iJbpik5@de-Yj=Kq!R<;s zWc}3q+a9{(+Nb7!^_ol1x&M|+=HK!8Hy*gf?ypaSc6v3iyzj1S!^;Ql zwfRv8?%@8iPh@+GdEf2UIAG^>X8wYCvw1tN;pJtJdG_`aL$xWy{lZoKm9uXxdmm;ODx?EW%5=vz`Nt@zScuCk)< zbAo9gVR#v8gXY{qwdVxcfHi280(=MV;mN~*fF3kQ0xIe?fFY=;BcUcbLydx)+HNju zVgzL7G*CPP2`B&DqzhhnJO(*ARaL^vC~MqNqk!tGqyxiJfM?mKgH$z5Q}7lb<$}S{ zgcqSMVqlgvn$ya{fQ1q)FvOCUDb5PpN<@3&Y7rgwg zcN~5Gxqs&U! zZa+MFk>ino1yFeUlF%19q0&an5D;bvnG%w*AWoSg5tvdhRg^*Jmi&_FQiS8ILXzgt zDR?Dn)I%ZfX)+$Eih+*S5EFUhEt6w7G)e4!DdXuQnkgHd$WfAMFY$U6)iBuh()Gdx z#e-z3_{lzS<_3)o0KW6Qv#x5!v&P66BYqGsgSz54fwSY<#HP8rN~5R#lQY_TlyH6kfy%91Q; zEc24VQlTu8G*w4bOJuT1S*uX_asVpJNe3f?DR5L^Rmlw;QIiJ?yMRejz%a{2QH{V7 zGG+MV2rWOFQY&(~EhMJO7zMn(GGMXkK602fLf)s14;R*+UB~D`jeWtx%N~btDSYA5 zFYwoapx}iJX@?IN1Z^nT*i!%m5X<_u z&0|fpj5Mf(22$jX7e|d5abWtT%kUdW=)QbPwE-FtAzu8841c=XsfIgd9jcDd{8kF4 zz^Jt$pB{4OrQ~d445a**iuRwH7)M%l=Svb`oDd&qBM&wQ@1WTRckB%AnOEQRYhm zOPx8AWMz|8Vr4rL7^fTw%wJkdf}8c%UUwA<;s`O0N;vPB#-d4gL?S$PLiegNAf8FD znJY41mWQ=8jw#jFKgqPPg|v9df;C`7N<(NYglu*~U_`4Wnwo=%eLM-G>G%>1ox0Gc zP*jx5)#9bTglT4qP1Q8BpGQKLCBNZthZ-_r`qQHhV2=1Pf;4gVGtLHOfRI_^6}eKJ z9@SQggiU2?0+^0L0L7p?wUN4%i-cK3G94JnG{&E{X)FR@ias@#qv`G>xmFsTD*MHT zQ6;30&wBw-Fc5wzV47J%%6>LQ|F!-#5i!n-fmLHve8@$}-za}91Yj?WE3@_h$%>|D? zaLqH1T>Fl<%-(CK)sHw}Gq;%M?6&5d-PS?-?y}zgdu)36ylvfC?vZTIWBb_G@ds_^ z``8}Ie$(7dLFPBiUdR7Q?X&$V;pL6fksH zXe+BqAyX9WZQ8Ke-cUoDP`r{H(dken(LE#N3A3OVgNTAs3_KQq>aVhzoomd#584A#s4he>RR2bRhH}7K< z6f~)FL6a8|=ABTLel#63r@ZJ$m|Ur&nXSsgaKUWh6;f4xDNu9icR{hmas|Oxb(KN= zX6~%sdES*`@=NSOkHbY~mDAL~1?jq=V^=6PnR0q}nP4hx&LbVCdgpd>;pxST8_m`j zJMwIi*!e4sQB_)2e))1vXnqOwB{X|!3qvn^YzoxO3+O?J%#?qu)16QzOSs_irB~RT z;wg7%1SH9sDtNKEFp1yIWnrrd!j(wClb9FYY%)xdvt|K^hYL2FP#(!moM;U}E8`em z@adZo-IX-;(mkF6D)QEwUNlq4PDZr0o-xKdiGLw+F-Vi~=8IWP@g-&wHrdygL1U?^ zm85{PlPMC)qZQ8x%gv51qm|Qw(NQl=8B`S>UuCTDQealA$QuH_YIgqXKmOH8{#C24 z;_^pRN1Clw&q%DW+;aYx*$40~4CFwt(kw6w4_RG!M~%&oGGVKc8i`;;&K+O63xItR zWd<>2gQnH!0SU)DYksP-4P${F)!|Fk5jrS13|S0(fw9SNhp#oDL>M_PgvT-qjtfgs*8@C1JQm)P%Yu5S3<3*si>Sn) zm0T+wjA}^`Dyo+zVLRCC!V$}zpe6y&9Q9?1@bCZOr(Qb+^pI=>l;E237FgB z>zrv3JM1IIr|t9J02vn{J32yh(u-#Y$$2QxEC5ql>F|Yw%`ziwGCoXAOUed?$w(LC zZl;i`Auxym&;qJlvrG<$2Iw%rT~3RH4hgxA%aT^pZxq6KWn5cBe5Zxi0k}{Rtza%3SR4EPG^Ru z%OqC|{~?<9;k2bv)p|xENhr1@k~J{8Gu1lLOJ4^?$3-Jb2!Clxv-RUx5DW>0x}@)j zb8aDDB1tBBv9UA!5|+lIQl>-5nuU7*-FG02Dq($Go6AS+wHfjj^O5_` zI_|)2kJ)bv?=K&|*9IW-ab8kBWXod@+-!oB=WT?J++*#xAH3x$hi`k_ew%n6`-DTc zIc%SeeII*+*Su(@<(|jO%PhUr(o2EP%OY25&ttn6V|xjfdXgEw>z*>q2*buhf4~}8 z2Z4gI;2vaG6I@A+GE7GeoP_u&Q=<$LrVNwP$CI<8yI$eJGfz9&2SV3)^($Wd!WZnc z!#1`J!fb3GEgDvrL6*=@9Bmfm0TWlTaSS04yu%GlIy)U%J?lI+@oUVRWa! zk%0rzj59*0WWk-~aPg-;akg8@Krv{HJf7W~%U)iFqk(9*o84&s%H`)@e(AYqp8BSD zzw;OmV*AeXnha9gAH`o7i?=n(VCX zNZSXhYNg}gB5B8HyGzG5warYs%1qgghn9ivLfCgLIsc~lxBBSS&D7-Xu!>O#Q@hh;*2RrS(^=CDjo zl8Z`4*ra3sPc5^A$u+S_Q+OPOgEA=WVx~!<0csLC2~$~9(ljZW5Q7EK07XcWJMyTi zouMsU`#_gJJAounA;$H^$yO_!wqeirnz^H0gj@@_*c#(>0Nt%-87}rSFH>C+oWPyE zJ&*m{fBL1r@6^QI|c=mKQqaaIFwJuqu%nheBCj_y0JxAy9rZnUl=$V1c~rKTX~ z&hX0S1s~j|?@dxwiPc22TsaCzN-sx@Yn2&eAsUFRt_D62^Y{W=5nai+NA3^3}Q6tMW40O-oRUgtxQ)sG}fmsk?zwZ zLc~;rr^X$#c~m6~yyUdjJPMeaF%J8SFqE@EbBs>d63YTAIl1GAel)$nsAZ&%y71W8 zFYRGt$)2zuh%Opmcrh!`$@r+|$wmstx<9LKAf)yc-una!v zVOI}g1OaU335Cjz?o5RZxnQR=i6-t4Mi@w75-UYb8O9I@BbF9q*m}=pb_L*wmB`Q^}1T z>M?Mj7;saCv!FR}OmdKnK|Kfvk|HVljxnPQm;GvRn z7>X}W8E=}(i8zt&s!^6lH8sM3r@+i37YGBOkps)d!bQf5gfEbpTx)*GYoe)1HqsYu z233Wc8b146pq!@A(rTELwTh}7;*FOAO$y2gPr(lel8<3cNlmHC2#j(`8}Vq$T}fl1 zCZ&!I#2e7adT!F?$~0MHs!bVQYow`!>&qAWf?=FBne2FBoDdNrqeGO1SL7HHO?6i- z$Q0_!QD*?l!wR%qqj;;xp*Id*!aqcI53t?0VF)e9jsTh5S1#+_kGW#02&z1YbYfsmJ z&vKX!?_as{%h0sB6Sf#wQ;J{TV&khMG`hS<)HKA_D}zdQ(5UjI)mZ2=tBD;hhGxpl z(jiBsrIIj@m*WZ2P#Sh(i*+X|-dfG{Qud&Jt4%=^&{b#SU@2+bA%X{y<)*i20bFUrOowwWM z&!mprXLH}jo|JPo_dNCydwUXl9eDYT`)qW~{+piohOJLI>UHR(!?r%|pjlpCK4$Or zkDa&i+Ya68q(ip!ee8KVuQhweHP>BjnU}xhdA$5b%Ph6@gq`5!z9YTe*0Yv>$xFaP zh!0fsurvq>qk(mR9t;TF1%boGnt;NH6bp1$f=^1{(v&`ofaB6dPW^V5aa2KoblL;gSdp zKn}(#Sx^l-$Lk{0Ft4&EI>Igq1_(rht_-5Ua`6kGU?>z)V;3_ycL<{@t5Shy->(z) z@J#8I<-(R33T%3hyA&CSp;2a>5fr}mMK37$+>e6u?}wLx;%+5hb-CBNO_tB8P3|w_ z!DpJj_jSpIAAZMM4|j|CqaQjwyzG%|f5Gf7^X)fY;r{a7w_NLy?1%5a-Th_zOi!h{ zSz|+D6QZ<@n254g-BDX3YFQW^3$-or5T=t0HuV~I=#w}tuxU6t+7cs-Dq*qJN}?lc#l}9$ zxPj-i9W}KPf1|FX&xqkG3y*vZ5R;nj#gOLku1ZHp7}+*Qa2(eZSX0JOP!$gaJRu## zDI%}ebQnfaB*RGIiG&5z`Mh(i3$Z-_!obTIa%X#6M;kns9v7+Sd(F&OLoP)PkQd%b zl*P;l3mNq8Wbm2rcfbAhLl4}0@rCERmf6|5V2K23b~4uy%tY*nlPTlH_%9gVB?`E3 z;P}nWYU~aaoiRbW(>F{KR|i!o^&KH7+maA-0ZW%&Rpo7xikU$XQWzw#hA%wVESD!a z!4sA`qr*r}c@vTdnPP!ombowsB$_Npj*{Sm15$_RwB*XL?jjMqjwGX%alFwng0$*u zTp4FGDhy2&rb)+0mx#gC_~KFOGE0r4uB5DqA*UlG)%a=qgJ&cQ>~oYC2L0|)FSCO; zOqK<}^%6;|?kSkEMlGx2Y{vD%sIrDI(%$59B!)sNZ~1EAY-%NGrgRZrdDErsgyCC| zs5}rh97#@Ismhwt8ImUZEC`iljMDY-HLI=cr&RARTa%PMr0ojmYUumep3~NsWy+S5 z1=dPaI^tSv_E)w;jw4lT60a4BSw~?Pq!Ql%Z8IGyB&zHer0#6$$O48h8#WImX3*wJ zwJn^bZi@wa*zI>u6B7opjSmBow8;mZ?B}&Is3QwX_Jx{63Z)5UjoF$w zRaFjxhz%2#?1zFDLmgAbi_HpAwlD-t4Pm%T$kEUo%ZY_Fb2oS?rL1q!YUN!F;v|PU zOEMk_nDN}v)YJk_$QOOFscKDG4)G+a!jtgGB}p1B3E7iIWP;2ZA>IOWMN{IXnvRDJ z1Ry``{O!;FhHE#e8Rrf)oPyMZ@Z1?oLu)n(8z!bMG#Q+XW$Kp@b8V@#K4UE47H2D& z@DGs@X-ciNSJgpeIH*w%LI?AwK5`7uL^P7UI{M2#rQi^m*UTiwjS7sKUwGkVb z32e4{L!AX|2xUWQQrKjqUWG&%;pv+o`;8l~z51FfE`yqNaf)!dxcrNkROOdi7jdQ- zEKc2-%`#puR7m2?IOWt99|FI8G01+sm{O$~Rb{~dEunlZQZi1V391)~YJORZR~0i- zRnnPuN+M&fK3xmCRJjs$<#HazxS9-7CH&hTefOt-`5j2v&1GO2aQ0mtjN51ZqBepwq_ImL0!TW5w{;Oa3Coi4c zRCWnoX6Y9UyzGNi;M;CHzYbOf%J6_KeCL)ZfCj9AmVhS^r;6O-B*cW5gPf{?Q_66k zD%1%lGNOtHKl`XxxKHU@pdg_gf``p^+sWn@}*0G(UgbaIlu5SnEdf`-Ulyx+*;Mc*r0Kr z{{oOd^XYTQ-C~BCRWCaKgUI)>(K#PH&0jD>%E(=2-^T`)@4w@EVA=g;?=OGz@yVyH z-MQ=5ntLbPWL=G#x+8^R03aFz` zD{D#;`t9hbrIQf~wyd_kcDb5#BpJPE;<*??3YoR7(q-CFI+dCXfn}pWVK6q;CN`ap|Rtl|LqqHB)2Pg9yc0-gGZHEhlBs3RsX`iClE# zK{iUAD#kDgCs#(>!WrPxq?9f;StjR{uXrP6e}qJ*tm;Uo94cEUgjy%@Ec_X&s_twu z)#cBSOJEBR&nca0FH8+<4U=mgIpWT0#}orO3y3c<6OyE?s!P=+mMuRWr@gaa`g9~@ z!L(8<35{!%1*RMddg;QK((yQ`<678GYrbUY;|v&cO~Jy@oqiozDCRt7s(OTR|TRi#c6ykV+Lrjyd!JXw>x~!-6exuN5lM=-~5u$`hWS$mOb(KqrE<0 ztxo1^>V}}@)Jk*o+4f8B~FvyOwJsTGhcZN2qc%u-EoQoJeOw65fG*Mo!?bGJ^q&+> z9){&WE`1@X8YwyNbfSSn3sTi>U-~7XJSY(fBf8`DrHS-njXaxlPyq<9JII`(@a%$z z?!WV{TS4aH)Qh!@&{xHi;FY?|SdrvWtBy=jAUC2IJchgorEZ!e(@HaCrm5U^K;bPL z$%mKO_jbRzwlMhR2yj*+Q57dIgNz6^=Pti87W{8GtRcNbOLKbqMAB01a8n8(sMQi` zF)y(caDBDnTa$Rhv3R=Hw-gCEVzyDM;vEZI;89MM^ru6jzc_JJ0*jCWQ;x5&q{BhA zAt-a9B=5Y08A7g$eAujGaZ+Q`V<$kdz8+&!`nAg6d%4l<6rrlg3sw%v2BkE=D1+sP zoy=m8UIkf|uNcZB6(e%ONE1p~YuU@}P?nZf(NJq3f-xyIQRW?C7$l^FsqaXBmU&4z zchm&qT-vp)3z;jCE1-*r>qYw$*AYb0&f#Z&^=!A5d-@t)hLqh$R+0eGZa0I>uybG1 z4i1mUu;Jy%on`lz-C0H$cbO3cJ#%yUtMKxp_g?+=`>yi-^5qx5=k;5y4%l-uAIFZ4-G5s*mpzjG#(g}Pz3E#H*#;ed@D^?{dkp)S{Wm$`kS(C* zw;i;FPh|Tuskgsj>k|*&!uPQc-D|@`=giu8&1F|!{(rr{Jn(YJZriOkb7{79gdF{4 zF!<&kE35}U;^E40E@i+C0CeNkolxKo#Df8;4G0MXqA)BA@HoUrA6|A(xm(iiU-L!I zh%fVOy~QRZzR674e#W0@*d(0s&bNnNl{OM?w!)_Z2$_mOV4$=nzEG{95IXM|7_!W)s%rO69-h@6C9rH)Z|jrEsz5DJ zEI*iAZn~BoY9PFKx^*`?KNJw!G8u~|0U#ZH*DJhy-p4=aVQjB0lVIRwz!^-2ktryl zW~H0UU-;Zd&N$_$(@#F~;~zfrQy+UTuzcxfKla&Ao$aY??=RnV^VPTAaOLecUgi7P zPd+^P#}jD6Cez{2&dsh>wL`{6(Gg^GLW1|CE4h7(4W|RjaYX3&!qXwNpJ|Irm|6#8 zM_dOTM<`9q^zq0VQ|8$1D&s7tIpR!{ePbLksG{`QPg!6#{GcDksLhuH z$6LCd-ZXL4B+$7ogZi?NFC`vqM7#tR=+`;|u4;hIMO&%PrOwQ%?X^gprTw}N=Y|c* zCwGSzf~K;>2oh(%r5FN_hF@;+EVQFw$Bh;Co#CCp*I#>0XCx23c>mumArDQtgiS6A z-~YA;R^5h!aE(;141`%D;)p?ii_Dj_s9K+WaT?qZkNk*)g+|SSj^1&0cv29OF0D_K zYwr!nu)rF*(xn8xKo|;ABv(f5D%0huD_6rIZ|@{#jB$};*w;}oY%+qx(0VIlVv|Ed zqaY+gUI>$G8cV{!jk*QHf~J%gnL!?rn41D-$K=+e%w2)$j#@eSRSu^PlU7vm=u-Sb z6+jjm4h2nFTfC%0fn3a}aWUdh!iz_fYDO}frjpq~7PEM=PzH1nq=n&J!J1NvlnxXB z8g}2^xB0(3hmWQpGcSTc7{nSoykj~>@Z2%WCMvwt_!V2>krSFw`LxXB8R3J>mW}+A zBRY7cVXFGCC@&v*RoQvo!nW#!>aUhvl^gLrz1{p`j($UdD)Yg&N#z^SIV(tX#P*s{E)mDXhH#>6TsS-k0 zWG6sxc{9rbFRZaMs=xl_KM>l@+5n$_-p6nF%2jTf1G)Lq#W_b8i8Xd=CBb2fA=m1* zk=->TvWj%ZE0F|} z!#k|;?tH?O;Q8fCD$KPRFHvoG1u*fo@BwDs;U@Si0=`sDN}!BK)8T)sVMKbx|{CjV`?I!C`YjI{--@IcQonB>0HC)U% zGKvDwQ9K4nVaPKgbrwuFk)_U3nIvuM|K0FNGR>oGL-G8Ir@<;qW4^dkMUJCLma5X^ zS^&BSx-kMn7)|tHaE_4aYX-&Bimf!)Egp<`2>%f2Sis8GhLW=GN-jDmEGLq9nwUD) zOAM5AIwbW~T_VXLN1BQd;}8IBL02VrstrMjcd*dsDAyQO!VV(dRrAOoeFv$IgeZwp zUxyN93d)>PaCX(#VeSM=fka=Hg@JFRx)%>7UUyAHfld0xRT%6e3aW(0rBxN**mI$* zV`IrFNoOQ|**91x1g8bNqcTw6#?B_;@KqPpEXzY`q9s%oydP1!Ds_2s?Q_v_f$PGd z)xHQ}T-PSTXP@qoYz%-5JHy5JXC9jmDR;XWeC~_c6Da-Ug8;IBmGXfvCB7TY&~Z;_ z1Iz9#BMeyW{bis0@;vs#cVFrMr0%^LxH)nC)nDLY?0t4w-Th_nFVEX!J>UCsbNP^c zwmj~D?I2}z{DIpad%!ktn!Cl(^EN;3z^#uzXsc6?+UebI+2izMcY&0B8T-gN8$ivT z$3FE9+w^_xBllj<|1Tf4-{$-8x#7(BzLpu^`wA~FwbV<_{=j=7SwC)rsE`_n2OW2d z8E}S(AzYFmvyG)|I?xBSYI@I^?||&!IN*^Qq01H>SR3@C=KeA~NJkYVK84DfvAY7gkUctioO5qO;-*x`E???Vp%J;r}9Q&5*F7tm<3+}na z_r9L^+T_l%tw`HKhl!1%O`r{l{Z*uD$3nqwlcw!fTTP5Djbm*bV(n?k36RMXELWlVg#K9&ticOH#&7~#|y+ZIyLv7}MZopOXHXNtb4 z@aB#+$VgRph=7j0w>(fIY;+9fm%-v3-m_2WWJt(@eVU6!J42TYe0u|@Ym8PqOs$%b z6TaAZ1T#g&LZfbg0zkus{7eQi^H8_bHB%Y!W$bsJ{MHxEe98ap_n-BffBd;tS1xy- zT>>0p`_J9W`y*0ha@kknnKc}+2(o~Q_L;q_e^d`sb7c5*%(b5yI5m9de4A}Q%B}gD zRUPuW3)rYx^A0IfK1%Ej=tB>hM~%=}U>SG%JOkt3e~htDFm;)8VZzuK8IFX=l2D}u z2_9kE>$aTafz6VSvEG^#fguqJfLuKE%OOcCN+jj5ff%yi7{%k)0AsWOw5Bj^&3IXR zKIaSV^NR$-CgP$zSV$RXfkY&hkk+b<=%UIOQe`k>-*}jT4ncLcKC1q{v3mPF>><_*NVSromjZ|pa1Xwb@k<6n!ER& zlV2fTwQ8=#z#ZWHpa1sU```QSjn`l2m?ICDIspLG-fHxe=70R|U!d$WJqdUGnDswj?qhO%$h#`aco+d^?z7*-*dZJzU%JWZoKJQw{fYN{gmm$ zeR%P}p~fp13|NOjhB;6koiX;26Is%o5v|PXh%lu{lVCKdvZg8|2020)2FU}MBpB5^ z21uDA9IkUg!TK~(a(Ux|9g+-ATV{kotvXs@9V})91x75bve!~HU#R(F)>vdicagKh z2p8;=7kTy@pg4=xj}`qF6}j=&s*4CweJS%|3FT3V7fHdW3Qv$p@IX_`$q>j*x7lYw z6UISm;o-+{@C;JZOPMd$RC7w9gs`Sn6A4~x)m-O-ya?;7N_i+`d<;aDLUGdJU6a7X zS&}Ira;v&cMRBScMT8&Wu@)s^s?imIkW>kIA!)pXbs1rkpj?l#z?=k77Df}ME~6x& zF9z=L#mr+-CFgWBHCSB;8BsM&P8af^SMo}(&9AeQFxV;b2IVwPXT*Tfs%heG<|@!N z3m-04MXnAmLy@X$?&RX~y>B2GIf9g7WC*$seDyUjFxf3;kQsUOx<6L_@ejZ8lfQfh zScZ#z1k9~u{~`781Go6z7vKyjgVKagFSy-bN-enk$_00P`Qf`Szx<-JPCjn;L)-

ckI3!o^ZfsZ#`s7_m_{Fx9LH9the`0uib6iHTypHA5A`vJ^4QN(l2JF(^62uN2eU*S@mVv?dzWX%)lxk-SMY`AQ!WVw`W4lMPZ5SX#;L%kQc;uZXxXJ?i z;sK>~VVpwuYT3kq=X~jc_tQNikg2GsF@k4P85uH68U%((G4@gH@Iv263F+&|0yu1= zrK75*J$BvEzS@=RfwjPk-QT#~q{-C7*#?#s7-9$+W>1_BI%wS~O*Na5XX$A+BB>b|^FS*m5?oM&R7D#~wkr1-LV{tN0$DnlQoq{GM z=wVychRwCcZVKO4*0(jBt6i9EDSZ&5jWmV@Y3JGy*klC5K98C>Ey=uKMCH8QHNq9m zNt7p?O071mpOZJSj$m-|mxZ!}}jQ(%hYl{^D7Nzz6*b52tWDN`^($A}WW#k1K46Zp z-2s%SZMNLhPTP_0r$tZ2$gTK|5V=|@4Pm56a_}TBMlN>A>2M*KmP#wcY7u-hD_w|! zjPWScW=afVPQR7FjwXbWVT7-VCWaABtu!_X88pQPj!FWju8D*!qt-2<=_?*3Q?-hL z9kZsDaaFC#6d1>AqJRpIzQB}P8851A;tA_T**tdU$uIjEXER4)#;``spbDTSri_lH z6=k{+NM*HKz!pH3_2Nh{g0xipa*_@OO=+^94(~;x2{j9vbmYj?w~>%K<3jRj5=&-6 zLo08Kl+Dga8q5A4+!gkftE}SE;I^_RLcmsd>%R6iKI`FoUu?2(yw;UPML6LcUHTH{E#M_FHd3bGc=gU3tajoxv!G1pSZy{-2-!>;-GS`jvmY%;bf& zOE0<*;AH{*AJ6?WBS##1(4V~Qr7wHQANvK5eO|;5fGgke_LE+{>dJb3>fDdoo$4a( zM)aOD-nr5W%k4RP&vn;ZYq{lLyxOZ)IsJ@NVN5x3(-Sh1x%MDb3BPOx}8 z;K$gj(%}?h4Nj_Ngc@GSh1TSf$+(idv15!~U-A;tBFtk=`VyiHvahdYJXW$BwSHuv zK+YhB4rSeWZ;W-9&k|)uFfC~Z2woaxn4A!)PG;6`Q#0@TH%3NO0p8}Z%mjf8n7#nG zHe@NKk1)a$2EOFzPLq5(3~LFE2=N`fVxuNVSM9<}4d0STLA0u17quk1)1=l4X|Rpd zq9P;-c6AKmbw{JxsbfTv-|T3jpJgtXMVcZ!1$LBp3i`5KFCC){Qiy3pMiDX20!_Ur zpt@(4BMJsG#!@SJ@u-f0a1o6NN1V)Z(ZDgPG#zfvGo_R@%6Z4+MYU*aC5*`F)TBFx zL>MLovCp`$JELlC2sx^WBvQ?(?wKv-5d*o4g3E*pf$IXk%Uo6RHV+uBUEux@UUqYN zP&c5JliSTtFMx=>nA}Ibe9;Sm%-}QB+_TwkJ)>S>c5@kIo`2gF@UrKzuld3U{h!o9 z`)uf+q-MYVReSEZ8occO^1R(QJaG1`Blg?w;9WQHiR}JW>X`ku@_$lqIb=d79K7|3 zhi>a*UmnT!acuXOyTuGIqc`od{?YqxaP*uFeeVlip0n$^yKb|_D^{31kG%+92GHyq zeTUN*Wx+;J4y1+M0=8fs;OTd7uoSYhv81m_Ne&f4rZ6uk2NEKHju6Ct|5@*Xrtt{w zLt3eY8C79P)($&l|IIhu;QyoSPM|lfsyp8cGJ~KX^H>xV6jU)!3dK-GQBYM>6+;co zlOl@qND>E}6%iB%P*gO`lL9g;ieuE)IK-HA?AzVxF=R3W7 zBp$w^1kNP7(Rrkmsy>38X9jEs!P64-#Fkb`aVd?t1j>r!MBoyu!Jm34dBr?r`O*?k z^Pg43$^@dZ$-*|~Vy(_-S*fC=3_*zy3nZ35^ihRGZmm*9?2`i0srR&s<}UhYL(V)| zZUjwyFGvFVqpG@`WPAV$#PN|y*0`Ja&|DE#ks-j4AH}&c{R-xMj+`K;1JZe$94*eR zEf}#0me&e{z>;>v7!VSn30X8Dj)+>+tv8dmlvo|O`r-@EJ&W_nJKf>MFY@v-1#tF^ z)C_M-xSFqBJ|Vbr`4V6m@}0fM z&ONfj&F&t0@Prgl1zvv38()J7|NM2YeuYe6*}Md;OIuBN@>!smx5+YL8I8&*Z3&as z5YK@FU-V=UH%shhjlPt6Oe9e$ZATh$wG`FU#8~0kV*wySOI24(KVfGBYBU0|bvfxdPrce8f0{YA7!pJW zu{27(1y5lptV)U@z(K4bdM3F<5ENT+jg_hSwuytuVPR}l$pRP(jHB)dN|}z_Vy%;| z|EDK<)k$%w7BYg;KXYXwoTE=lnNQPSEi@4f`kq}}ddd0TLZZL730OlW;B4>PI_SB! z1+fW7ON|#DtWPNC*U8}sLB}vM0B8h3Rqwm<9Y-8;;8vS&GHbUTjR;6QXpE0JK{ud` z*l2CwOA8c^Ti#S%H!n-ROP*C8S3W0Moc2zKLW0idO7ru9Dab+&+`*sc$%6o1;N|w` z;Bj9mFUD5f^nyQzVsnco*ff{V8vftmV`xU)HZR=VYHJiT4qJJa1tS~6issDT&A+PG ztzPxOm+z*b@z?+ic>e8w`j6g8JnFE6%_!9AIllm}g6kg~uK(f2ZxX z!R?K&y&S~0rE5g{+RvG*PTgad9laqkd-q*7*p%(q3c_ncX8e#`4wEPMpxFo*yU1=+wQ z%2S5^<_bvp?%Qt}FI2j`=Z;&ya`zqmAlrRmx^Nb2!%8^gCP!M15WE_(oUDz%M1Hub zNp~VnziP^2_HH*3c)G2?vOQ(H$#5~e{ORkj`;c$H*pKirQeN2&pHPHSL)w5Q(Ito{ zAyeG`Y2Yq|$OsbCZCC1&{^-@kAuODs#D#$z(MPP7hG#Wr*C85+%(XjH!MXZV(wUlZXCT{D{p`j`+7+e66n0hHGtw>sx?l^5HooeRUwKU3b zUsW3Q(lttTIXJj1N4iY;5qF6Ra3_wQ#7fcQCX9;dKa@dPKocvmv`keniEZstRFIE= zO89hN#5fBRNce1c8GXdsE}(ZQlg@Zb5IQYxTEn3~%4y3aC{0@ZO-BtqnTR78WNDZn zacsqi(@i&%9u1#vdkWWNBJ!zZk~kiMVU&w#M~ zWq4WacTtZ&c*oZtzRO|kKmO@=6m>ScO=qzBv9I3ldtdPKBX{5U=)Ir$-s892dDHt| zcj=O43nz!Mfn|`{dF)e9+-Ld1x$yGprH3th$t>a2qi3JFaKH0UJ@nGmN5AT<1(%ii}9_yzNtg|mcpi}zZ4@?8IuT6NO?#~!-twp&i{vh}z9 z<>#-rh5cnP_N+Ch@h}%1e>7-jllOvi*K!WIlprUV3>V9^?F;llbGB;%OhsTa&t$FM*6zTfj=vg6kFl!XpsL&v_14JOOC2ki@BwwU2%VEO6~y!GO9mtT6}s&`)T zDv%jo{?xVa{rvSG@e5|>u?6R`y}bP7WA_W~v+x-CW_f*lMNaI*m6~y?OrPmCvICnW z2qljZu^_FekX5OKk|ia4%ZM{C&Dq9Hf7X|Hm3Coh0tF21)eUY#dM<#&dH@i=o^1pS`^IW zXRkS9pS||laoepoT<>{&PJXGKWrrKz{f^(=bGMy0-((XrgF)x0s$QgZo^Ihxf6jkJ zylYPdFMIigPR?W;;sh!|4F*1J;+y>TsvzI(w%O8@XfQf%%`$eZ)||1zPjAhNN==5C z8>eQHV~;-C$BK!Dn#G{BO+ZE%fm!&R?9egwu})bBrLCCq*~cC1=4NDfflXR{=qEJ`ZFb>sbX45*Wc7K zQ~}an0j{f%E>n?O)FxWKAfZ+UC1H-z zB{B(@TCM-<30`9DWHpt-wwPyLZJ_|IR7sHPN~Kjn>=^$&ci!T~9G3(s6^N#=SY39& zmMm2$NjI5nn;B&qWvXa0iKZM4gFu-<4X)Xj8G zGk-UNwmOo>M?pFLN|);-tq}w-b9>HNd!|~w&amx^wuCYDa5>I(ThpCh_7n?b?sn)B zO~qQFEi^X_4Q<14_*8gGla&LRXDd^X=A{Eu-1Wcs%YVnuhnws({=IY2|N56u)or)` zJ}eOS@lhk_BeuDJ=8f38jGvtar3;WC z1;T@Ep^B&59yryap|*s}AR)GbG@*cD%v}oDsvBo%WYr>_DKuYB(&g)oRaX%~wP+Xt zKJqb-t^63GOuBrDA&waZ<`UPRyDxe|Fvn#HG!x64?Pm_&Q%pl}V+3O)iHZ`Z)HpJ- z%14%2#G0eDF(oaBKGaJ?Y8X@L(3l!=<62BN_nlUPOT_7)s#-x$wa}ofdi3I|t5~`L zD-F8^-y*<@8p9y?psc8|FO74WsGcC1YK=JolT=FFP^OA5g?J(u6(jDZ82GxnAfb)I zbIB!$V(FaPE@d`f;VG!g+YzSqOdOSD3MpKRs3(Cyrb(vfQFI_hX3A$4)zOgKOvsY8 zPJWGziHgXE2+Rk6nW_w8d{l9nIAnCg*4)@_O1C(OQ6iWq^Bnuj7Ko>X){f`G%V6?v z0rHQ&?J%|@*`3X{kLzQ{ciiys-PixY z*KhmEtyjJN6(=t~W>??Gb{^X&vL|49(H!ToPda+?3Nxg<^r$_ARmaY882ggbkGgdA zON3XfIZC*6&EfW!A?2~Z?EPi?%WD_ydG<+rpTBIs^Hv<_{pB;49(c^bJNLb>jh?^W zG+u`0@(jHd2o!T8Ejr+8cnz#;a~Zrv$!P=$rNKkQ0*DvZ1MXl_Iw)m{8-A3(w)1@cET03Ri-bc;W_8+7XU3 zC`w{`%5+N~uvb1ENTjWxF8D7WLk@6wnJYqsGy;^;WO-G}lj`=7%MlYQpsASw_O_xp z-u410?{!G+r2|=6Mq4~>P(@u#yeWx4<+BoH$aP8Q;IOntxl}|em-q;^Ziezb;IOxq z?J0+si=EAu4=i^c8(xN;eMbAL_r3X=4_#q%*)OGhAKUxOz_M`DXFh6w+0U1SuikS@ zKVP=R%s*+jg~!Od6F7BTS*{cpOW^5=bAxmF@<keVT!bkCq5EbRf;SoYE?ZBDJdSz2~taG?z&o~sU$~WE~;`CMz}MP%k}I=Y_@=e?sLTa8u0lmYY@(bFXyn7I)5+ zYnJ9^S&TTSCR>i2M{Qmt^a?P=@TX_ih$ptB3z>@ue25URrG$9NqT_7??X}0Q+ix@3 z#=Y|Gzhjwr-Q}0Ph{wO)^V$@4BpWC;inWfy81j6L&8@fEj8pB^5{|SXeB3cdYA?$m zuqOo&*_;B0eKSEOc-6iYDP|izfhGUuul~b%XRqdpgWgyXWQ;>$TI7+P!?8_|s&LfN zYx@Y4A9z{2n2X(n*6teE9b^`5XAO1`BmzX4JrmKq{!mAXa>USC;{`EpZl4`hdKya4 zikE|RSm7xUB<Xbtwc@F> z2{ETLJ{dtp1W6I64@1>J3iLcALGtm}MzOS3hm_%5j|<#iGZ^WiZyZP=5fjQ}D9U>( zh#=|Z1Kja>6Kf*J^}Pp@jAmRF1gUa zamM&fufN=zO-@P}8i1#VJnktxE@l?GNiZ*ow4&s2K_ln)8JdZi*aNF+YmR)z;4g%#p8owSg*f@rTO*`UKPdUl=a_L4TPa!DdpbM0(6cC1C zZCKlrwKwfZnH1m0F33A+m5R;hwVXKg7C=56GK$#+_ zO3~ztSTnnaI*dS;Hg-oaBo6bQROnh-jTkiZr7~O0P7B;|=Plk=zWJ7$KKAjeJ6P_d zJ&^8Q=DY5`O$^`JU)BKtz;K_NE-lp30Z!9Xhe0M&$O7tW1+^unL)FX6#2NIJ8qKKN5uby8H38C<0yl~h+Np6ZqpL&PJbOqw%MJ-H<^d+NY~yQ4@> zBc(1)IzB{DOpvBPx5klmYS00H1lggcl~z~KGz8N$N?qcEgL_ISi4@R#pBBP&Vq>IX zh=7K4Q&BCMs_PpPI7Bl#34&2qX$T7k>3|p?pv#(^WmG3FVhKg^U7~cUt{~x3CgK7z zfdXkV$j}s2QB`7-lvnDq zoT|rD4$-5B)GkGouPsB0A~`+zlwi+-G~%j=k&%vJtridUP^JuAj}Ky(n7BdX9u00v z3sph(TzI*au2oO$Qb7DK;N>85uPq0eq2@pN`S*VLhu;@vWtYKZXRsZ~ z9xpHZ>KCy5ok#A5mmj+8(?59P_6P3x=o>CO<@h6aSaa&URZHe9U$iH@Y>Ro(OZHfJ zNo>!R9K4tPtDVxjAW5dgiWS_rc9=yDI@$54e&GIv;vlq=i zYw=!w!R-Cz3r{;_?J4t?FWl#)2kyAd7L)yDPGER>gY{qF@h9A87ls$g!RAJC2rWsx zO$Qp`KH$`^Q@xs_+@kJ?qRpci#z)Ru?GdrUHxrCJYXTCIaSF1c%BLg2@0f zHC&R#ccK`q>MI?{fKWhHE$XR=i5#`GLXf~&TfF=T`Er#V^nz`*h@R*z%GgTdNeU)v zdH#RU{C&6HYAd_sIBS$=3z$%q0xq$ITm*G(NvjL>Ym59~H62vMRwngmRWnh_#6&_7 zRuUPogVykJp6)cz7kme^vAAzW-SMXQR|RP~2)K?2q;n1@ z9^Joq9-AY|;pFJ5C6FSAe=8xAuZR^9e7U^b#EnZ%U5;ou&y4c}F&TVtkmkW6mZo`O zaIO^^l0!~C&96FfDx%L6GNq9rPSq5^N&@9juZl9!M>8rWD29VA&IS5Q5D_=&WU&?d z6r@R4w<<6)ouL43Fpu5iug)kM$S@;KaA01T{`-*HM?*uVP4&wGj3WaXKjsly0`aP=Pz@o@vQ$Nc@~fV4Ypza6kl z2RN74Njl>ghNc=7)dIBrvImsdY~zhL-Do4zn&(|uu?gI?==ftPu+#RF!W)&}4FdpZb=F1mo~&S&TJuI9W_it|^_QFBl}O7JcY~L9r^4dQK!-$ zMqe5|)_p{iJ!PtnApq-vfF@T&VVQyi0x?S5CTkqea0V$e3swa*DhkL*6jLnSQVD@e zPZeD1Y#un6C?qgRA_8htz=#$|#5pG7^h8rYDZ~?*QWbSM%mr!p)QuCPp)g|kHHt)( ziu87BgqPiYkl8bk<}S6+*PLbyXD39i4pQ3Fr@+Ki{cce@)WsBs?afr{~h}I1v^s3;N7Qs(zLN znv*ZoUBHkV#``i7$r_iuwGp^~2#_g-r)(v2)a}`|7wO3H9`Rz`grA1<{}e)GU&uzdTQjKfy*_uG$)VvugI-g@RV3!>GTsp+hN zJO^)8c(IH30@#|7IDvTL)_1Mk%YA+X?TaY;w16H54 z`gGQVrWL*IO|Ms1pb~xl*Z=(Q&PBt^wvtyYT>=P0(;A|PO-y=DaqlyG5ATnB=zZ_L z_s-kg9Y`6-b)@`jUw_2=%RBA7<3<~=&n7Ny!L!4}6i-1sn23E`PEg*=e3E5|7>5U% zhvhH}&Yqd{pb~%rm&5@>))t_raF%@c1;8A5S#Mz+HF`5fQ-DqrjiV1}_SSy=LfG@EtI* z%W^uBZI}6558VkeKYGvS9=Yd+AAaMGhwlE^n=fB}!cjY(xokgR*)OHMzdZgYM{`oZU~JZR+!dmVT9PDjn(e(TMj1uyduU|TCi0h*SuFp1?ub`Bw1p6ou$8QU?h zy!0X)qu?!k2ju~G(&53jaqZ*=pW#*@5xNwsg1(JuIGuxPC&;dmhr^IKSs_R7B9Y}` zGH;e6;g~d@>OzhoXfQKGNs2UxN)`+&Z5tOKAk;3K1QC|kH@xu5Rd}WA|{j?kT|!>cY*jJejY6cbKCIzG;8|m7 z0xyg4B$r8Jh^MBZ#2NaQ_FRG`wM7%;(^JC*34uOdUIvG+y;_qe!Jaat>=kB-4}aht zL1r}0Y=@V9?8_&=-u&A0Uwhd)hq3)n%5S7V<}clN?LD`B_Q88^4ljS}@dteD%m1Xp z%RvlYBA*2BgH6l7o#c7u|bR#o&vv7THO(NiPDOYC1MD| zp)@8VOw&xK4d-0bp@vc%rUMZ}2V|5Xo!={HDc6_3NE<%$yoxf>R73$BMlK-f`TH8SA{%z_vf18aI-71p z$+Tm|P>BEG@BRj!Vm+U9@Od#srUjWGl5;OEJuqY+JA+(vLMVXI76cY>a>FfuvrKDbA1tfIt(l5V9U zJjIHz(g#>WEtxueBCt{#y?lbwN?Kq;JPX)j1*qD_1vyy^69i&SLrkJl!6hlxmB6#M zqrs}i)>5*Rk{*CXCb$t=H0gme|M|j4f)r$G2?U`>X0)ON-eKW3mlKVrMiH$yY9_g~ z>cq7U09l*s@W&7thWL2kV8OB`;wGP-wWu^J$)PRd(}xuL)L4^7rcSpJ<4g^;#=N13 zN;qU3ib1bdD7Y8~g#MCDt*3ggHCMNXL+QqP^6@85y98adzN$Y<6K9M|Pdis65>cP;)Tnop|LfoV zjaON1q=L!2?zW?I$l&4Y-|%YgeU81Y=Luw`q2Zaw$WN1Q1m!d$Gmc1igS^vi z?W4IzRd&hYVtUeFe-Mm8_mK->A-2)%$s4SSo9V*q$v%eAd&f83c!S3aCM*15TimwT zWJ4Rl8p4hL=l}5=`@%Bk?Yp-(L7Zl>gY3k$BKVi9X-^qFg7?1b?J_YjQ`z~Yj0?v(uPd`R=zfh%ILEe*4b>YTAblj=6+=#4e#Ez=uBSvTY8nw7cL^ z>_&8Hdg+NA`>0Yy7*HDW62e>|tO(o!`4e$7Fd=65#(=X+nal_g+dIDZ z%Xd1J4GX)Q&?SjjL27N(f_0cXJm674Tg*@)9F4f9p$sRcgZTf|XXdmvvLJ1Mv4*fo z_!l1uRpc<2cGXzQa?y4&|daOb9|C3^+D0Ac9PX zNqnLyU6pcB3i1gO#~E{JiVaG{F+`RQ5+fi}k#vbzp%LOKGd{^Gj+-?4+8Yx&iUwz; z)1lJp##35mB8sQ!@j);Fsxxd$XSXV5u|Y8KGL_sTO-QSecA0JxqgI)t$q3|3t1)Cf z7haa%V^u4K^};3hiAVHYczM8NiGiJcL))HmuPWO|_Q@}}xNm$7)GWs?voB-YaTcC@ z@D4|^pM3BZ|C9Rhx9)!Q%b&R7b*r4m_I+%JvHgGfqy>9S!ZEX#pRoU$Wrtt9>L_2v zw#)1*+1_7vBzwhzSql%|cG*jJnLy@~_C9;*TwvLeY`e@Ct~$WSv7N^r`^)|(wfxvw zFFk16Bj;`RqRl3F*>l?UC!fgPu>EDbr9L)dhsFCrK1h4$LG!kM@r$8lJ3IEBJB$tL zawaVb0N~<<3nd^#zz_yh9jt=^f$OzruCz0&$d0NipyINV7kMq(=X91XS%{S?IKZ~h zC2VL187o`KRFc2}UPdpy@dg`o_=?x+MQQm02&tBnlkhB{>it9v<%la42UPjD@T`iE zsa8OsTxD`m3S4fiVAYhTzXSlwO+w>QfamaZ@Eo24qahY+e4t&=u~EX{;b;iAj|=gi zVQ@|eo`_MV26Rj`D@iCK8h<X&bPfypkN$F}usmsvo0-PLdZ%qQOK=gTj@VCCE1aLN1L`T83_^{K1vCf* zo*ti2jDUx!C|8qv$=ehpxTA^)QiLEWRe!FSpl1yRayq0fxdLTyE(b#ivIr%8S|8q{ zkl(9Rm0FPn%7>CPQ;|${HLWLa=}cByB~&I#u}j3-7!y9T{1JwCEkXZ;nv`%>mmvA{ zDR2?-Q8z=0<$x_3#lbe#bX_mu^~lSc1>huB@u?z)5_qNbj&YUYY zISZC2K3i@Z&&@l<*3fIN`rsi4>`&;X8?L|SZo6?=K~Z(X<~owTK>;GzigDb2G z7rn@XGIg0tL+C7Mg_uXAArmKdW)CR0)>8#d<0+vLGpV-LyC_DJ`bIZ}p2CFf%qHeo zp`u|!p}Q6nF?%YdjEZiY+bSm`Pjib#T^u5aTivzl?o%%+VWNqSruh z##k^I9k&n+Ll_6v>yuL;^NrVk(&jQ>ND)9sTMa>_x@CIiG{t(BFhcR{u?GF?hm=uE zPtF;W)}Qt2oUz=?oH6NWxb%Pa>t8`qZ+PQt;K41odcj5;Z^XSg;K2PZyy#r$>979! zFSzmid``Y|$at#DkrViA@swyBVJ=^6qzFn`l=g&yi8Rds(FDt} z2WTL(S1p{rHYsVC*e{~aSh?J}YY^DMX0IfN6mI$A=Qr7KLoYmg7uaSoTN$C69y0yZoxjSswx zvpWvfL40yZ2bTNNmwb;`kOtf3zD2sH1$izALK0?m;?Q)koXy)mIbV2EC+I9Y1BG~CcYC(nEpi{FtX< z3P&$4AP`6tFdv*{Hj34v8|D(DZt69IrRPu;2V_YcTBTB%5hQm|5-lB>B9}sTAgiEs zgcL@pmLO5iAgBsig^XVt#nH2N64A&R+`6od+OC5rl2c96w3 zl$`B6#^H-+Ji#kKgs}M{fSUOw^Y z-6r9sv%<@MyL|4_L)M*izYguPEZbo(m+QTn9?cj@-?SI}W`(AYFy!Mw@96x)(p*tM0|F$pO^jYvSXtw;66S;-nI)oxQ zguRn1$DS;_1~bB*_GJ*jxt1h8r?h-uyMljVJpj?MmwC(F4_f*C6YOax zPTN(aErbizT9jdD2%XxV7udrJUQBk>6J7@Ao%58iBMe`AK0qI+ga~SQ@<0bn zpl_nB<1Vf5+%ubQjKg80FkGxU1)y)zxn9nJ`-3D(&m36NjZl<@om$r%IdC$#&SlpI^kHkUu~(RcXX7s&jHkG|`}?|sX)AAX0;W#9Yq zODR8Jw#Do`_9OS*;j3SFeCac}yKS^_NVt4ro|VDEb;(^6a}@btd_qI11R72?8VP}m z$Qu+SWGXTM4KxgMCJExf@^-j${Jh!3bDFm2@=(f zxuRMmCS4J3Xyi-OI)WIAws<1Y3Y=*|I_IDF$ip{x8L)bz4y=<4cC2hp`N9uvv5L7g zCh-}9WTuQ9atoEx#N0?X(<2UgG6m4v)?qF`Q!^Xb&-HepghBLw{quiUS~Kx+7jU4k zy0vIn5`RIv(TXcT;SYc?OX(n+_R(slJm~j3{#K(v>Q}w*d z(Q8gMZ8g@1tQ`w6uGKPTfo>zwC&Ufm9@cp=1pLub%CjV%<0my*hd=2=;6nlM90zC% zXH7_ibQ;#c_BzpMU~02rJ-=>GoExerJ4nzU2bVao#x!U45s0838kD+g>1-=gLKV!V zQw&cD3e!-K&}Xz!CJu?Fm3Fz6p;2r}cdwPY3rr!Oo@5!ko)X9xs3%hsv<`twGlsPG zdT0Y|n$yw;F-juRmLOy-jElga%~~Yog%_o;9-s&U5;+Fbx7M7)Nk=Yti9gc z_vqM&&|@8z0E#udKA<6nq?Jfreb|{UPoW)|@Elvy=&4-r=)0(fJuQJ1PT+xlB9?W(t054xhWcGBsx#-erd8QX!JTU z^*kpu@I8)ZC>tMWWE!ph{h^{)R4DA#3=;wyTG`D5*66?g+rLI8;HGI-c55_x$8wlU z6}OLN1oJ~^-QrFn>t$he-Bz z9nXX*2bl)C@2XIk5$Glms;;Ivxc<=nU*@U-0=fD;bF;9;BfvPGz_^4cU4rUBO(+0P zmm?j_QA+|e2({tUrHWONBW<@`f<$Q#1OR`0q=^7IgO)+vfr}wf`7Q-Ob--adrx+bz z4QyQsm=~xd$QRIv=|-GZAWLDQ5lbTwBot{EGV1lv15ItLMbYCXfnlXCQ<8=~KyVqL zp2qu{HsE&nD1;Rz!bBiFo@u41Ko;HTARifRp)HqbhydGKG|0kP@(07GCCF7h z`ic;PoP&c)0UxC@v%|D9@|A{>waz8o5R9N!s@C@S$V7%)3|%JIrS>vKH!D^&q!QFQ zLaeYIiVdL>aZX9%3PcDVMASMwhhgPE*L|T+r5acp>4_mG!cZfL7&?#F0HLZU7>T1~ z#44tzSX#cZpjK^{Lmb7#l_skya)>i|S`N&UCSF0prOYuYHBFgV%po#=`eUxIU4|bD zfAljemzR}HFP6(m{Hs5*ox}|`#c^-AG`~HG?S*BBu|vmVco{l|k?lCURO+<0OP~Mx z(YGIZ^uC+F_lq762-=-9p8 z$UDronm2vk`kRAhmJj>6XRkfo7AJ`3NU$yD7j3yE6lryUpl!K&VGu^P<7xi4vfF&& zPDIewDqqxVy!KnI-PV1m(30e2^+o5Meahm6FW77|e_numKX&zpZAg9ay;trxXSRjO zJC7dr+D>u~>go@@|05rKpMy{S!Z2C0PFn#}|K1f>cnA!}n}=-@D?09&W1P?ShLNv! z_%sIy4=KaHFuQ}ADjs$CL3XZr4puR)0{yuVaIW{SxynF<@A=39R5iyoZit(wE#W;c z92`p5Mrm#dm~5jQ9|#wi)gq@fIUX#-bghy$gdl>)Z*uPxk+Ln{@{=w@(@9_gZk|{0d>kO;^I!i-zx49@^W;>vcbOf= z29sfAJIm4%GM&vv1~miJHkUtn%{wnS?^JmCx~tzg@Um0cH-G-)cisG%`|tYV!(X`_ zWPbdscTd724{-LxR_9z7tKnErEuey>)uAwgkq;pjiAxL4OC6J}I7(Fu109PSpB&L}o z7HSdB=to=lCn_RVibeuMK^o8WtfC^KlU|WHwi0L95S?M`;~iU!v%~r=^-YFR;ib6DJ;fEqZCaCGzG20UlAMPqv2qtOFh{r zd+PO#$?sIWvhjcauYbagO-!oXDisq-c@

$xW70ZDDOOmzXQd0VdZJZ5HZHVVpr) zqfZq!F-!CPZoJ_JL(f=7W2Dd@MliQa^G*$3yHS3@46{(1O6^LAm(5_nK7EW?Qx*^0 zgv7$&A+;%wo3X|{#-ssGAJR2nku`z>=&6ccb2JLK8mLA2jeO49C82dP1vz3SCB-`X)R^CftTrtImOgXbFHRunM!LZhIMi|j38vX zc&a;1hFffFEaG_5oc`u6TFFtDxyX{Q&u9YOu#zJdGy$cSQrji<6scwE(*)bl67p5Y ztsCL)V~64JK(xceDtS3vs3m5 zKlq_fMLjD1*0;a@YpL|4R|T$j^1GFeOxiDnY*<)ZjhsYhfQ4ytE% zdNMCPy_ca;p8P9PQDb34oxzH%A{WIbHJ*-~`30mWY1UJS*g+Ad#)JacGS1A>|9o#$ znGJ|=<4DJoV`KJUUTzPRZBnH<{f#3`%(Ny7(*-^hcGKBVb(50gZ6;*ylj^(kwF2-#KBJq1aN^OxK+S5Bp5CpaJWrad((jDfTBtV z0o;L=x}hO_2oi7>#?S1$?wxkXDrbScR0+FvzS$ zAhX?kTj92wwP?CD!?Lyj0mU;VaifeJ`6DDfRTZg)10#eS-4vM_Hf>`VlcbDWIe1Pd zw1o&ch^s5DN*u~hRcV>=6vwuXks4L6;vmRw<5D6%@`vT0*6@nNMBps0%Cs`1RQ+iT zWvVD>RB5$FP>bnm9RhNNDDf#w6DLKih$iZalUVbMcdXTFRLAlo}0N3wzC=`CiLfHSb{w^QH$#{G{!aO-!ze%BAaeb23*|G@osU32-x ziw@p*Q=7|vCbjU$?N44X>(t}+T6WCdOOKv&>alZIE!-DgzTmVY&s}j?c-i-^L1tii z%_(5=e&?*1cg~9af#nO&IOvi!hrVphp_i>b%%7LfTRQi3=N|p4wJ%wH!km>y&pvkk zjsq`mWa57cFN0$oOH;ZHP-#9ScnEA-8sJBlfTZOElmqUp*FDPShue6y79+4NYxkCq zicA`cwAt#LU3dbR9yU7eyzgGK4m)VRo#ksk@*yt(+ZXnx)vR52`od?g_nMC*WHyc+ zc>;vtWgp1(MGiaBK34)J-*L;A>@WMP0!&RS-@1gLef+cci6B$1y9JRlj#eUo6vTqH z6&&_J9$fY89VWGD#mTn|Y~_MU;bmL8+S{pVWT3RB#liV%xR|4EtC#N)6YHsV0l8W# zcpMF+;NTE-uR3DoL|=fKR$L%x7jjP}@R^l7^QW5}iz!b4kgt^J_L1p^Ion#+p=nL9 zINF17N1$UH$3Eu;FGIvmU&F`%veVZRzV~&_M|_|BJrXWu`v3B$u6>uo*xq0M_($FW zFMsT+ciLaRN~^9)VXWpTu~JKD`9^7JR(Gtws^jBQqI7JZ77sGj8^Ut%X;7z3 z+Nww{O;UqI3DN~*s#A%P1#RJjAt(&#_!P=f1y4?bfKqiKh7y>|L`h;cbeSExOgEV> z#pqdsLlQ%LB*d{5^G_%yAa;qf$qJJs_Fk=C#4!_lGL!S{0TKRK(OfO*+{=vMQWMzC z^eBO+oN==Vl1ZY9*^Q4ahfr>(fnf96p$Ou5;QUDw0{zkO;_yeN77r@>sMw9D;RIo; zh(0_qNLk>{sp8(%Slc~40q`ivgCmr=#|U`8PO}irQ#G9Fj}olLxZUKhWHhq~PwE}@!Vgx8_rW;34UNq^n!i{uw zNwlfTO75x4+E`m$s)Ya_N@f9U@l>Qbg~geRG7`Dy@&$DTb#c==ff_CatOTX{o{B85 zA{``%!=L_)fVrlT^~{hm+M-c{imGdfbQbGQF@->Rfg0?_hM8CYk%@!(oDM1y?UF=k za6>6obpx4HrC}|_fj*ww=<1#kxlK^8<3=de^$^>mY3meXukmU^jdE*n6RWE$8jr?^ zV;CNkQTVU__kR!2S*&Aj4eE}-db%_KZnRQf6KH~`*12RQ3F@V}B07xW>=qkem}{@Q zX3~Ht5j%SQ;%&DQZ?NI>9(wpb`^bOvOaGJg(xsEM-qw8L4}a>LQBQpH$;a(41J3SR z_e&yfnQ`BMa9RM(qvMncvSKwaLnQ?Rmd)8KG z?|_&C8P@G{qm5;zrU{FgUNCH)w4kf2>t;jzg)TLpAf*jA{9P8hHOLS%9k~?d7(n{E zXnnG7iAI0WwX}uM_H8@(HC08skeEe+gs_6Garzi}XF2xCsc{6oRTN zA+VBIccLDL%`4=&=$vq7<(At8%U#b`m*(Dw4&W(HyAM;?T;toT>)rfZcf_iLX#omx zUOnbEY9VXD+^g26umBxa1+}U;$=AjiMA;^+G`tzil%7VTZ9oGcq^KC^fLlZ5PvcC7 z0wpCXRZCr$Raewyl+tn(3Di?XEOWGu9crVZ0QIEe)H!e(j$LMl$vfBmOuP&n6=H~| zP;=x=7s-r9f^KQzzPJ~izD{9kNDP`74ptM8_1yc*>Z+Jf72~7sa|QM<#CEg=6JdxR z(&dyM^wFp&Uplo%UHLL=tSZwmvx>zCx_3|Glm57MHxUm_BnmPU8i($9YzNK%uXaTo zhnmSm5~D2yP3KI@BE81Slt_OSOO#pvlrs@DbGXI8H^2o6lakD4Br}OiK|+ztX;2If zx7}!}OptUO`P@&$A3FQ2va@YPG^1IsU8yWqmp zk2-7FL1!;N_`H>eoWJVOb5882U$*uz;i5ARdd=A{x%7-fE?O}k zaK7-=1J5{auXQKRJMMs;_SkyE_Lsf7JhRU{*}_NE@Z95i5G8Iu6Z2g-#tGIB%{g+Uf(qf^|-OaX6vK-~YmmKG)-LwOV$T?GyvWzQWDywrZXH3M5FZo3*Y59VZlqc+I+Lme)<#UUWc@O;!*;o1K3_Wdg&2|`fG;oSo-OV zZEL?^wk>WC*TnAevWLk~=fMXY;B{PT6>a^;yiitjI%tLY1;A(G(-jS5~a! zR7Kz@salSEdD^_kuDcRMtFSYJBVaTpRlW1r-UMf4E)C}&w`ECVQ#X@Y+lxf|x79+6W%8^!a1edB+yc%~0- z2_n>_K?E|@m5K{;rfXp$ z=y;bMck~rm&%RbJU+$%e@Und?+=!qsP4Hp)RKj17W70DpVuj(fRw~M2knXHY16&Mg z>d!lFx3w|F7d>a)I*)gZ6yFr$x~t;W@I~ER#Pk$6U~%h)qM;JD`hZv|@0XiLF-0^H zY~mQ&TWq?q5pA#-Q^@L>SlOlllZ_X?C?7cQS0<=pkkU%N7enwTh|%ncJDY)C0D+A! zI-9JRlw2Frm}{NtVGz6a@j>tJt~j7H7>#8HG}SPRTZVsa8F!gDN^)iS%ot*ezZ|s` zF>hU{JFX!T?4uMt(Ts*nz))s3L>8s$h*^yEnwIJ0BOr)ptBAI8;v=8UTkKdh`@0le zQl`4N0T=Fqy6zosA40(}-O`+@`iUxdQjbB}Kb6*tR1_$z70jrVo{A8wPAl9L33QgD zbqMfRYcxtGp6sR}ltGCheQ+y{k{atL%j0c;N!anLK}11^A+XDq8A(X%TJ`@-4y897H*H&fcC0)7DC-Ifg1G zmN_dC)JX@wKJ-Az>83V1al-*y$}43|q8YgxqzRPlZpv}@GlI1CVrbNFzr*&x^1?+6 z{buMt{?(t~ea~&qU_-}OedPU+vW?~+{^+|u`?)VrdE4}R-}&x@GtCL=?h{J1uE>(? z$r^%QDw7&?bN4+cuuAet%mbb(5RHHV-6jOcbl08}D#e7J0wY+vJ&5SjE2bu%37g>7 z1m&Fztz%e2k2nqY4p)b+t+_BYC8)!h^dyT8D) z+eZ`nXdQZ{kmG(4jXqp2N1EG{9dTe;Oqn>~34_!GFb57|^Ul znV~J|p^0>=rht5{W^I&mvMMzK(PG2}l+!$u^2}IWnaS05Jcpq8$P}h|#zaoGni%1! z6A0aXMKYO=4bx%7>c)zyg0@sN6vJxLE1r7!H!0l(l+S9Pcn)P!m;w@tsx_pPAB}j3 zD~GsIg{(T5BDD&G;A#A8I4N$8cDWo{#d%uuEKpo2ZZr{Gs+im~K~|B(go!UI%sfap zKQt-1l-cZL?lqYR5+)p(wvyXihLoMS{>hIVMsgx(f|p@rmx3ah$OI7Bx4i5u2aO@+ z!1Cni^~}$tT>8wHFJrqz^Nok^6rOnamajkj#d~kP=9`ax?k%rAb>T~P^06=fUp{@w zKC71=aK_^ORvfqQnk9!^e8#bttUmhEb;rWXP&2%|Zt3KI%IB^)`25ojhL_JcX|L5M z&#}2|XZhvp4i7KC;rs=cuRHRB<@?)ZKI8a3Pd|3{#Vd|D;h>IWlJl`Ls zoXeizWfQw?Su2s_$Ch2wsZCE0a-F_5?L*L>U0ZOiAofOWp4zyvG<#HQ35Gfa0MIwM zU{zDP?ONDbrX6QApr7N`&S3j1B7XoJ{`7U%f~n4J+h2am8(s?t`(Ac98gNG7JkS37 z?1lK<@AzFGw3O+bws)LuH}@X2ujVQp!N#{K-9o^@BhiY<>!2I2!+b8Lwl8X;hd?z1 zA_iKyzVIx}3)+hnX(qRXH7pZ&dXfwyYnnCJ0%X~R5j-nq8B3}?Vy+Yp9_i!E+p!lW z!Hm2SFDAp_g=rHt>uc8#)+-}bcSwasN_ z$Cf+f$TuMD&1HDmC%?R{eA~^Rz3Fo>zW+y0PG!T$Hl1DCgLZ6N?0a7~T=$;KU%vV^ zuUPA6QtM)JB&TPj}0sf4rBYPl$)4KW|_A9S=}**Yu6jfGWdQO;|%+FH0VM=vcFnUQ+!P7Y zDKNQV>d)s)Q8)9#Y#|%8l(_WPKlV~PjmTM zU=+D@XI&CaBFD@aAw_@2pD)T?_hD+))y=JPVlx9$RlBgQ3n9o#slhV5vbupZDQb07OdyHUa<5JSC zT_iR{Dd?6!4Xy0Z8IO%w_?p{^xq)dPUn`I?Nb3l>nTTPtp0R*Gk5oct_N?tST;{k7 z_%IHM$(3dYar#K8SU{yg+2QIW3k4f2zEEOfN!Y5_iCTe%1S%w*< z(bJaBXb|J9y*Ohl;htiW4&Bf+L54<9MH?xAhI-VX5)I|6P7Q((kOG9}`Q)$Vg`p zI%B0TnTJPpS|#1^WHSC)CIy`_m9xtVq-|nFu0lu!zZ}*l+%i|I%#*Y&}p?%OmlQ zPI;b25D{*Ny7-f*UDz_L=1@#RTM#fQDeSE0kVO+ypD{=Q3q7!W`KvE=(AtLb*PnRg z&wl;KwwMn(c>lfU>;WCizwiFLoy4~3{EcruHaVyL)LhNf-FJLQ#F-J$P%d(HwQmFzN4P0z>qhAxWwOcGa_^k{8`CecIkHL z8*F9B_x>`shs9D)EzcI17Za;C|{Kf~bv~DpgcV=r%EzJ_CAizxn66 z>(Ve6%{ck|bC;n}WD*cA2fm#Af8bXpvgg9f$btq$pu`+2I6^{QoiYV*keGz0&RUlk z`?CWePbbjOM=i`@iUDUNFf1q%f*}KIBu+yFX{Cz9(zQ+%IrJPwq@=JG0YTE$l^>cz ziT=6pavubH+L3bl<2D^|C}cDZk&rXg8;Oano;t9N02BIzm(kGB9dQ$-3kaq;PYaz+ z*NWsyCxuzi4AH}4@vM5KSsMXaH6i6QETfGU#c-&XZsH**R+(4NVPs5X#wyW6giG2g zeI^I_1+tJ;C95J%3WA|O{)A3fK~^owmtQS$JQ1Xu%RxM0%XgQ}Y36n-Li3M=%O>ZR373NP!llUyULJ2Kdp#Ls29pEPVDiX;l*=4I z*m-Ol%H!kMKs2P>32k^;koFeyw;#Xvdr#c=_*cL5_=8_~;-N46;$MBm_py&Xe7ni! z@^QPZSTy^zllEJF?Cg_{++)RY`<;90QJ0^$2xN8`d*&@>FESqjFQ2vSfb&)zw07x! zr!Ss;=90ag$G&*=At1BZ{xZC5XL;S?y-q)Fw`E7|blQSFUvb9Kiw@as&lhhTUVgsq z<)`qnO%_kEEi@KWFw@fGiKvIAFeaA+mINEE{(MC^8nQ#V`6e5~r~E5uF68J~qm5Bd zv*m!y))WYI0+zot`F@x8jt`nQcZ*Fog?XK<7VHiKzkskm(KxMbE7^X==Wh6v?|eFs z?JtKGM?20QRQduKh%NK|?|x_gz*0Wz(MS6t<(|9mZco;sN=}HCh>~qNIRtKr0q{LX z1&uA59uC`j1E_sy3;yMM)8CqCj|pD(lmH;5w&%|hycD>5r|n;i80J+Af7sT7$RM_^ z7zZNeO)2HE+y8=&LFsL`-rA~dW6KN3gksAMo;=&AvWzmaPmZ$^ld?0{U2}Zp+j9mg zQP`Ktu$nV_Hnq{HP7@U^aNf&~UZZ+>8Ds{W{dW0|TR-RH*fy7iHk1R-eHP5Y?2ldb zyFU44bNSVmuDR-cZ-tjXdChy^<0Lw|&n3viFxCz5mW{Jo1(Ae0}2YS?eu= z7Dr3Jgmuu^5z8?^3=_+%^;eFC(h_f#v{F}!QjTC@McJjbbp(rY%dyz9j)RKm@e~iY z!KJ#1Yhb#JCNc~WS5dw&>Pje8q)G}9L1FnA3W~~+Y2a%n`)h>+eQ3o=c9|O>6WI_K zLrj`6@KIDthqluS#|ll#qsNfHK?j*Q$6pQ`MqHzkUK5xab2+KOnV}>fiRYG6z10?z z&(zpie)+{0^P-JY4|co}=}kTUEfJc7c{ee;B|Wo{fJg{L&;d`kh1MhRJ!5R({yrj#a>7vm;& zgTy+F841c`0ZfRHNjIOFB7KFVVo=yfTQx!)M~PbpsGs=rK*sK zryxQ38m@{og^?jvQIHU*CzC$i-&mzKaA7fGaGYzqS~yd+2Pt~2iCv2T7EGWjC!kFJ zPJ{T8Rd&FH@;mRay>9GZW+0npa#Yd;TRb(B5ikJ{ou=?r{B2%1d6CM)TZkAYqK|ci zs^oeg4A3dHXj+$CVgq+NI0LFO<1=qLBwdwY2Kkw+hpqdL(7(`jk;bT2T(mcpd# zqFO302PPeOd86mAZ)QM@EDfoljW&OG*mi5k6bGD*Uk~xjMb5xjxV74hp$TrQx~5GE zxap*12L|L7fWwgQ=9_H7)TT|+X^6j@Wp%~`Wab^2ZA=$z#US9h4FZqQ0czhI^DvxM zm9m&dIi|vk;1zfw%WEL~lX?gOQyH^U2Dhw1Z9xvUtW+R{o*`C#Nd3o9m7pXGU`0<# zqIa|0GC`5P;gXd!0b8BTB+xHBxIG>Y)Pd>a77$8XIwO-)6?Ns{rujOv7!8Z*Zx~XZ z=FzA{R_5i0bISuGdHBGB!S6^m=bT%flP>`sgd>8z^3lr=DM%v-Im(=N`{g69I!qVL zEK!WDfX{^0%%7^D&ViRfW}%OAw`m(>jv&QSMhq|t`0x@0`lM1Kl9E1XhBBB8z@oH( zTdk{9W;`WWM!JUJnF#b+O{-}{Kkz=JTxbJb{?H%)M5tb1%xP4WZsca7iItvGawNJJ zUD9M!iTN~tl<`ON%uwBMnRF7d9SpOioOq%v#C9}+O;E;7rs{E|c%1YzPbQ;4Ob z8O&<}1tbP#WGSo`;>>s*%&t`2oi?8OtW#92EGu1%yAyO{bc~z zscc8D<%gH;FH75T2ASoL4_~*x+(xr+e~mYqJB%HyR{96ueBdYFd-SmfzWCL9uYdB< zFa7c-_rLDaC5O!2%*)FsE!buG3A23fYuSQ5;bku`pS|pe%g#O?SoYN~-}^d$)!`SM zc7(&&!pvdp{cJ8fk_|6=c{#{@;pqoDkL{CRs~67l)vr_icIuc}FI#og;=}gXYr9Ru z%l;=d@UqDh81xX^XK*Z|c3^$}%gG|UGyKC2EyI$Q1DPC1-lJ#K;IP@--Y-AdL-<@} zD-OTQQ+?}{y-`5hlT1&9{p7-0BY5f9b~EsM+|iRac7OM+Z*s~SF7{G0DDB*}-DG>s zK8r0V0-wF$Y%;9D^_# ziQbHE_t_(5m!w!9t-sFH@svnsQcEY3QdLFGvCbl*aVEH2-dY4UP`^z@o`+*d^EXTq2ftP`0?=@fZp(}3u)carmsn?CbV zc=^uXzrp*+b@{mWiFy6vSl=fL~LcWbXpuO%2sphyCqeyQX51$S~3kHBdCR5 zqIKWeicF9YFt<`$Ua{3`Imj?keYg!035p2!-R)#$46V3QPXh7kTO2{&Hpzjycnp}1Th80jO)gW@dZt57gp?M zhWBL808~Y6)B_-#WC1tTHI20?6Hp?{NJoqt1>_49#*k|&cB+$o8YUTvZKs1wEz&N{ zsa^PUVjEwCB9>2QBZ5n;2WDFdrHC6Ksmf5zKSZlr+tXjnhFT||Rz`|eyUQ95=Hp-WDk`Fe zS)F1OU^ML&W8Ryy9>6-^i;w(J5(ozT0%d4qN?=u8_M|Q4C3ItV&=VmmMg8AmApzg% z?FK@_)5D@xC`+V9(T5b(sqMxwBZak2qg-a6`W}f)(cO_xE*cXH4$fNt@vnXc5!+4P zckXORd8eJX-D~gN-*&|tW&Y|!7?CA|< z!-T~w^nx}HtuS;N%|mjfW|iLl&>?A?Zd@G`lp{Ud40q7YrB&9W3C&d$%*Oly-4a&S zTf)n&m~QR}a->;9tlT9wH+&z@ilerD!Or3MSH_7_9J@iEN_k z4BSJOEZk(Kb68j!I_`U4rCkwce`KbrOZmd! z@HGA|pEancO3V`~M`Fm*Od-xGT&#*vKM}JDG8L1HCq9&K*E?=@hrf^Nu5Xz&kv9V1ef|QXkR86PP6--SAe)G13OO!$5urO%chBC+;Ubfu~ zHQQPKlVAEsrcI3rE*{X>iR%ts+ea2A==fU?Ik5fRZ#*EO$d0qkF!&v zef)tNfBJ)ayuW<-ycYn=!m{J{ShaZWX^Zwy}MEj(y%Ld(HDNIQ>9)*^%sXSM2}FHHTlk@_@6K%sGGAerr#hV}JSF zlMg!S@L2;dPvG)X`^z>royWFDc&7;-^f;8`%?q{ig2PZJDC%IO-4-hXKJvfW-l5B8K}jSzEaog((XLf`eWUhKHtcKtod{&G3cu5IN$iwzX}Xtp9R zEWcn=>(cWa%$5UO1JvT>Coh)i&1FZpk-^b@2h4^zxC)zFBYe?ovyC^fn%GwMG?kKC z#PjK}bVsc$tTL%5@SwEL(`iRoJ;CNt+n)k;!F-$4wE`cH{ka^ru;nlgHNxXIsVQn5 zSCKV*;*1E0*p@L77AB7oxGN4p5wYy#ML^BgTWx8@WTh=O-N<$TRvcP5-D1RLa%mGk z|5kIf!hNy7X0djU%J9(AZ&Y=-2^l?<@-vg3OP8@NJ*_ z^atK>`8n|Nhu-sMc==P;zSsWp-M4<`u3K*Kee5sa{`rT#eB0L^yvNU%1q-xb9TY6G z94c<4w58ArYXNQjwQlC=88|J|iVP*IwuB&o5)H}`Z&LcXMSToNtC2Z;H>xR zZ$_4557+{6`0!)(?GEtkpL}d0d(XR6Tz<3c}# zi&gZLfQfK6Ot_#P?#T~FIBcnxzVHPvFkZIYd=rCF4*_pAoDOQS5>~#;=jop=#}l^G z#H*Z{_9s1!Lr4uwz!p%2bMED3E;s&^RO;!Oj}Lgfh6bhPvn_=)7v+&r5-YKwlw9{t zzA9QIQ5BX^%PnSM&aG0EI0)VAY@Q_%t0gBxk&A}fnkJ!0tUnm4&cHZ$X=e7c)9lT` zRz>rvWPx*|1r-|zVs+gJYM|tVyTsEa5yWXDo^+N@qT5GpJc*;ZnA((-sl5_72&!<) zwaYZ9F6o+p12X(w5{f1p5+{v7(0qK9V#{3ZqbX${MffXqsZ_vFj6aDP3Jsom3%PW& zBr$@73E6atn9tc-PzC?qRQGZvTDvJ$dmW-_tNs)G|CRA{aV+V#y{2TLA}}XrMc*V^BtdbgUG0 zyECaw+BW=|i!x>axcL1axN^6etKuO)^LnmGG{X|jR_+~fn!}Qa?LK?Dj(JXkNz8qePd5+loHyeR znF_YrVpC^VFrfpT-A#AgDDb^6GZi0+OTeV26#64j*J*7lxF0uTjxAyy1-aVD>=FfO zIT?fn$gRIok{!aT>XN9{dZsQ!JuM~9t#oTh!~_SrX*gm%Tqd@dFcCgl&F4`ajr&Wq zyV>NXu3L{r&ll@d*otuw*KfrtGPrq_dWuw`1H)24d&Pm8{P{d~-Z;-(n$sSHQOYxi zbj0DBJb5(mqa5@obMhtRM1}yRoM0>nz{$_y7eft-04ptt9zpxa(|9@T>EFr&GPeg^ zJ`Mt%=|&A4q`NsEB#2TuKnZ2;D4~GSQaaB~8P=uxy$?0$f;&={*2&U~nZb1a% z+B;nbeXzjPi;br^F&SMLJ zv!6+MwR!C+^WbIQ$M(sui_Sdc;?;-RT!xpO$G+?w-vry&$G+fY-}?fbFIsVs_m|H+ ze%~cW?78>$o3+0@jhA_Va2$7+6UfoB^I{{_Q)ml~nG?{nW;xttz2HzGwhe9Ow=VF< z!~th0()n#?|7<+ThabJajDtn$;Rn9rK(+5(JA?h@yKV=W;oN;@?_me|^{;-V9c1q; z`>T|1g4r?_yta(L!0m&k{h0|mzVDtp{gkRq`L>}^UV8Bb&T`u(CeeCo9r5fJumztj zvXHOk$$Dye^26nxx7#q%5cD9b7~D|NqHhBoJ(R%$+6aA1w?plDf^R5UteHY42+ZGx zj+LsLs_-?5Oh-C^Y-fs4=K6mqyASA1iz?6Kh=8J^m@tkBl$>)Eker&FX`(~{5d=XH z5KMppOytnyoSG&yp$UzG0UZYwb?1z`vuDrF?3ta}NxQRQ_VfO~L(PAhaGZ1M)LYN3 zTle0&RnJrR_tf(|ulNLmg%~KF!tx7)(gIp8wYKuPSFOpxjA)l%Faw5gBckS*4Fm}! z8w)EP1HO5|Ij5a+oE%=3JdABa8EW=m_KnwFX2%&!_G&ZiY_D1FV_*J%`K04!o^{&M z-}vfjXP$aY-^cd7ue)!(%AYUWU-rE(kl7>I9?Z5@3Lve07C}LgrQdRC9keW397Pwc zjMhpCo3qC0gKqsD@vze^HfyV;UyjqNO{P^|URmC$r#8uCVwMz0K$NgyK*uy;hnAPHBWPQw0a-6hxAyS(_ZP5)1W_>IKvQdJ;tgvBZVuQ?1Lt7*! z3A#7+Jf&bRdGf&&5?hLh^o8fynO^pA3xU+>JHxV50Y#1>YYRe$P7dd&mp|3Ya(!Q9V2xXB6cQHL5(Ykmy|uK zsufp2e;xw&h@*3v$0RDq(Mm6vRtpWxwH9#dg;R3^rFzHnMe^jEGl-3s0>2e1yMUMp1lF|%qVtp!BowVA{-l(h{H8f| z>?cH9{gOyfm2auTS&o^V5(Z6l3LE~RlzAjex{QZ!bp#v3XmLoCPNh*p0yW7neW^B+ zjJOCN1)U{Y(Mz3zhU^)Mx;jFm(+yg8J?C3XgF?Kr9CI%&9(kjx{K5Pry$dSve9E0=xvJ+MWmjor=HtZl2g)(kJ+#qLABiF z0}iU(C3B&0M*qX#{uTe|k%rD+5Lj|Ln-44psL>%=x&HExarc>PH^W^>#X_zgJ+N)y z6m-0uYZqG#-b8LHT8{NXcie|@O>iBi#U%@|3Gb3(9J@9eL}7!5wBTf(`AI$_b=l<6 z_Yyjpl3G=IYAlSk=|q}yUsB+ihS&w6I~baCBNe8Tt27>-G+2adu8)*rZf256GWimp zm@3?e!lh0PZS}%js!A>GPGmujT6!~#169$8)43owl>Ug_qfvlffJF{`fi|M?k{BL7 z!cdSOTc|`CQRwbaS)ssQ!vR=?^lB4Aj)Fr(h1zOWit_v~e-2vOlZ7aifBnLrK*|GM zLdpPj8_hkNO&VRagK{jFXUVXC7(;$=PeGGfDG$pdG6s>U@GlfRfH2`W@z5w?OC#n~ zXyTR}jGVQZ7qp-3Bl`r?i=WHJqmQa|WSEy^o&nav`zWvyUGo~_@GV5lRM`_>;x3;WR#iX1woIn&x zwWIA2sGEqI*9+yEYn5lFdk+szhQ*x0I2|IEl&9C?8O~4U5a{~TrOEY2%+S*CDmkzW z8q4h|gTs=7?PlA`(-YKO`u%U8^Y_Z0%C?)_r@a(j_{9%;of(MsB(^Q)Cmz15^vvV4 zfBe+_kKTX%&z_y_{pCAvy1@TQeSDpF`9G|-hwQGf-r;+1=>6sWXRK;}`M_N#Phv|mcUi@Q+5S(;&hnu

2{d3993w!OaZ3307%+>U)a`J z92>ZP^VttRo0qQmEnU&dVu?8N8d6Lxm??-xfa#1Bc)Es}d_l+RyD-Jt{=^%->@?J!32{ zvNm8au>+PtBIYSbP`=O6f#>(p2Fv&#WZ~Io@Fpcm#2zLpfqI}AEQTsWB0LLtn&SU* zb|3!MR@J%pAK;;ifQobk0TBfO5mYP@do-4)iN+QiHWW}S5V6F9y~M;=5yXH>@AaYA zNYG^>(==I- zOsNucgdEGW0*B6}>1&N z@Xg-P%xjHr$qa_Vi;zN=cd`(=HrXadXLm5&zQ{IctcHR1Z= zL-!Fcca7W4ttzDv875_GB3-5CXc^5&%i>idGb2j`u%_Fx7?OajY-`>od4mL8TCmoQJEV5G?UY;0 z+DOL8eVP0cP{bwDsP4>nGR#=oYg#V-lSm~qWP+qjBB2Xs9b8mCr>$gm!tbG2r+wwK zLcsZ7fuIoRftt2@SuLzs_ATZXxiy6k6n zT>HywF8sl7@4E8-+b(>|YmYy4-)*0H_~VZ}c-OslUU|g4)22=P?Yi9w&v@!zyz=OG zzW&*Y<JcFbtxn}au}SR=VHK#nWfw!fq`cisJSR+!%#Suvr&tn`1NIE zctOoVT*WdY9ZxO1C&CEhL;@gmv%{lyHUw|ExL8ja@`7P$f+iaSqi|A%G-?*52QNv# zGX|NC5Vr zy^7^;edVJbF8jT&>n=b0#;bm=UiM*Z-(OZWyQKZ=kYd>_pmz2Lj{6#~tDU_H+AToQ zWd!R+aCfk{Qw(*dpdv=AsjHW59k!Ga-L@uMN8%ndYNt);gUOeDS){<*D%FV&QBK!L zSeBf+guN}%ogSur57HqJgFx*7Fmm0I$$^rQ(p<(eU};zrB=Yx0slsv%P|U?TB=lB;yLZxb$?%%>9DYWe z_<_@>o23=V9e~iA^%8)&#EgzaD;7(c_XeZ|W@%;Uy+g4>@)BiyCzfzCWMTb70Wpe) zBbQ-A2y8WSDLNPoOR%UWF)pP9wqC4uj;(DeSzC9XOhhMW!O)ruV^Meo78<5WF^XG6 z7GN$ckaQMEZ`R(vWy0QSXmvYy05mj<d1t0bgi==-Rx3Mb*3v&ACEQ=Ptgi9OBs6#-$v`^j;-i3Sr`DLKHI6a zETX#-{`ddG+0Trl1qbMu@Ir5?#MXt&y1egb%x|B&K z46ovrQc(&-Cxwd~l6x-UAx$R<)7+)qBk90P+ENAMTjnK@y`a;=B;pn;a5WTn6C|+( z7|R&h8M(Ch0qU~&gfjsifp6mYTYN*|`5Kagp=e{vIE$Y<7xglj*B4JYz*wA?MM+2* z9tv8%6uyK@vtW!f7rLQp$uNaxU(RBM`JvAbyIkjjcLBFSF}7XUB`B`*uI=@$jmv1a zK)Xu!JARJvM%ky^w$uG~|C~kW{%tO`|9_IJ@0*M)V78jxFOabGM&P{wz^n;lQa4MSGU47j!-#I> z?U`1l?Zr??UPi-k0bmy^i&4u6++s4;?e$G>lounN3muY<8K$8uy)=e&;f*dy@?|$o zZ(9^F#v~U;f|F#&;BV6?7M%QhSmvm&B4DAY0(xp@s1jhccD_M@?6ERYFDsmt$v%VK zmzU?!vyW%1mv6oPm-pXt&6dk9xa;~0$$8)Z(!E6eoKl+JJJ!ntA6}IEnTa!QZ6WQwDs!OGr!i^|7)oZ$hz7!UvrMi!zu2deXF_m?d znUKo51V#a<=!2YEyZTr42k9BE%9pPqQT+&0MJWN%S>jPtic&49g(+J``o}-|VP!8- z+d^{WgXMg+!CnfW%01sbc_O{GDPsuWs8R>flZCkItXM)OK0aL zLrX!Bm>sWv`V7q6ODS3y5v`Bu7=o9ESwt?7?^)||Okn&A1MzWd#d1IYrKJ-_^>UA! zJz~~XI}<~`zwGz1-}jeqAb#)b6Q{iO8)u#B?=OGv+h_dz>@Q#btG~bQ@^f#$;o=AG zy6N}#-$7jD)xxfAkAwXfojRAr1+Nx%U$B!(s+072Cw6fW&T6iHUHg>MJb2GC+lfCVijgav{m0f6mS zlX)wEzb!o&l6+@<5OR;F(JYf{guF@>cydSyAFWoGpen2N`QRawh6O3Zyrw%0cqZ-0 zm#rbf5@946YfY_X6m*IgS4DMP@1FkkfC!DG8mA0dTDRfHX+{Vk=`+U`ux<%YCiynB zOl;|nGhqy0+OCS;k3?i?F)8f5<*~>_o(*x=wVo|$x;eV$`$Pfq(!)S$7IebbtU1D9 z6OmBjEdN@@a^OYZQ1BE8C!vE0S4mDs>y99F@4VfT2z!%lvPI$BjJOnwrI*RnII@>5 zd~f1-hQUzp<63(5c<6k)DDM_FytBx)2ErfKkt_SS+8D<5q@`bdL>=ZL)&xJA*AETj+ zQyo)CzPY0Uo0mXnLq!Cpn}QbzV3@NH2|E$Omv9YP2dyrnXUbk~>%$#Ba-%V{ zP+*fhOeGbZiRQd;93Vb78x53B|qR8tz7bS;Cf=`sm%@T-+>-99{pm zW%jySdb>?tkZvC?4Hd#Hq`J(uxm#yLtM#W)zDf3*BhzAGs*(#Vuz53S^rzoi8vLHlq_rR~m$p5hfT5C0|q|67C}tZd`aJ zk5Dfw0n`O*0cC;;q9@KidY%0Kft!6E+oNO;lyAB27e0DT@7v9)X2#oZyigr|_YId^ zfAP6@UiXW8ZoKgPADwmYmS6tv-b>&7+Gjp$t4)VJVcTP#{Fo>2x0Bl0SD5#I%(l-x z>fqPC=bO(i_43nBeC7M!cEZW;e%S}!@uJth>?zNB+GC${)ZWiO?!f0Cd+?Em z?!D(O+xq_Uj(=#iylIoaB&B4l1g;|MIi|9%s@5N3_FLIXLWP@p&;wJ|oMMnyMW33K z9frFh^)CxT_^LmOT2dj*E4%8?uZm$Xq*T<31)~HO9VP~f5Y@}dTPblN6vZaIpY;}6 zrKyWYL5cwzWr|ZNWk;nPh6KQ%i@|8VlpZKo>Pl5g@{|d%S({hWmz5|0Xp05O0-*jy zf)EnST&zjOyR<1Gn;@h~si_T>1_mi%Mti}}e~Je!3ojTi!OG%)`LmuKD~hX{r&yj( zXajh%+@ojDoxk|0kACg1Pxt-h554CvPXEX|Klb4__s5rgh57t*zIw%l-&Zf+dfkP6 zfBCofxQ16A2fMa=xa{}4C@hu6F8iJuQ&h){s$(rLE{vqE*3IGz%PZemzU#q1Q?>HA zd2wEHKkC{OY!~ijo5^ywabLSnjkak7gK=iF|2RRh*eGCh?c_)ts@sv=%SPjDFJey_ zd%K4&LV6oi$6!bz8D4D>6r6UmvMOy}Frq6K94vLsNIsD4EV6=UWZ^;|cZ39tNT5lf zDj8WxAV7KQW`q|GvXok)*Z>F}pxmO>GO`dU>0*;oAs8umwDh9D2n55zw>%>OU~f`% zfJU_dx#y*u%Q8^z!LkpG7Yi|(&>6F5iCL_Sd`;F_Ix6;F4sDC+1%)vRM$bq$g$Y5? z^#Yj`+LY4pdiDMVPzZo=6pl+5gHc0eMUT)>!X*Iva%gEUt`{OgX?qoPTk7F2iis$e zZ@lhG57r?i*(=C*La-2BzNk1F?IOe}G)wvCQes?S#wl6AO>J|!eu6syP*+7WWyH)Mu?@$ zfEgzT3S=avW2k9T7`45W6LG1;57w(mVUaT)mb%={uN4GDAfz&TXCUV0aX+_VN9w%Ad~7TdZQHKR2FN< z8^zAB$4Q{9z*y)+AXF1w*K{I}zf5wJ#(bkT`d!d^}$pq6HWj8NE(Pdo5VqCfy8WEyQN|IyAr&t!h0Hzh4 z3m)L6Mrn4G($-BdB-V|~z;FVSQPOo27jrTuJaYb-O~$l$~WR7>njQ(RGb^0BtdS?Onax zvIx<=+`c3?78}>hNJ=e8S+%#6xf{GZ8JiI>1p_u--m~yfzznq-TKdd)e1o-ruJLL& zwt9qav}EuUvE(t7663r-wnwouf(3xgx1fw=m0e0pLPp(LCEf6hGiz+@xl1pGrdA*c z&nQ56#)tbnwnCr+xq_m)V6}F>sprdDpTX{LNcGWcWwLsiMX|i)n)5weX1{OC<=0(w z&h;1nM7?~|yM8= z=nXG<+6UkI!js?e(i7kO{8QfZ@{hd#m2Z9hamOFE`?H_6=kZV9?>SFD@M%xlYp-3m zQ!M`%K9Ai`G^+}gX�Wk4lM>jYW;7uw_*1DI}G63OD7R;#Z+ZQ5Y&Gm9$}aksgLZ zVxU(8_T(Cg+E4-LE5pbss!lNcTIC_7a#D**Vin+uVql4B1!b&TRxEhv-4LXcerN<(iY!$gIGAF21)^h99+I<`yGb6{6mbZUrP#qV@EF^#4Iezf^!4HSCz*sI>sf9y7}} z1RSc{uXHif=QPeSwq-;*V-dMK9Quo*S6+6ZXLbdOO0UGVV={5f6KtDjiAgl%-x8OWG1{7E71BN|HSu?kuK8%1WLlWt81a z)i}f`7iqARFfe3}0Hw{V%pnu@-XH>4VHbv>rrZkX!jy@2=F(_{YiMUK>+J$@Oz6dJ zvfAFKL@!Y5qcsIDt_zg{*0P5wJsIXz^438JIwa1?fBAahDZNJN>qTg^cuF-k7QXmJ zA-$lM+Jdti6_J!rQKl<|aFYfFIgHlIJF)a{`!FN%wCp8Z8t_dGfI9%5LQ^CRj8JRQ zl@hP03v`0931xvtCwIWME|U}abikN_m|jQE8k<;Ga%6by~R_lge&o@J;+;T0Yx zl9?S#)5k+0khN}PjuyWwKVyq9=>eN*6IqjL=`q6K;etU{6!;PdV-yJ(DLs}jVA3#3 z=*=azz)Sql255+pFB0w)r&BKhX?79--cZUjUoDKeSS316qccH@Wa8Hvf@Gu%6vG7N zYY30SyaWH#JLw7~$NrA`4)%%jJehj7l1EYF@b6Q*Ra=!>;|pI+5mgmaj_F$6EY%>@ zcVhvlvItpiB1+{Imol%NB8wuQFovr8fKg#srpF?pNr9CKM|yTa0~?~3L%Eq+(M4&r z?yLin5@08MS&Zf`CF|B|FJUu5F3X_0V3Xtwl~ped;}F8PZiY&mrP0Z-xQr9jP@j7* z)^ZAksBOiX5lghdlxCNbE^eV234NLT;JHt@ka8e%GrD4vZj-KdmM(aT3w~}!`WkWl zcgw)o0SJbuSYkuf9m?)25^fgvX<`V*zOh`irZ#P9n^EgyW$5q#bT0?7Xkcb)NVn(9 z{mTp(d$(=rHD7UUr0Fx?ZfDufM3xhDkZC!R4Q`UybHzV?iwZyQG zECIc^A!A^%J`eYMUn+vBmz5IpSWS(bD2;oX+|OnA8(%)2tzPa|vbCN#`&n$?T)y$r zpQ@MdxbElo-E#gl7k%e1Uh|Ayx83xF-8b$3xJ`TSvPr$X-)>v)v&*K#_S@wJM;-9$ zKYGf0-}r(Lz3rv%`t#?0=p8Tp@OxhNrq?|EkB-~x_@nlC=8=0o>&X3%eDa?A?Y`sA zoBu2Ivf9m4PKBiUPDMskj_R@sMul5dWRF=DkMOEIb`>0BPi(m>rP%?vS7NIWS*Gx0 zXW>E=u%1UNeyb&mNN1;4!)irASk0?=6tN;5t*olGxm2GjDm{MXQV6l8UXD;b`?H_^ z@E8AZPN~YABPuQCe9=lnV$w#1K@{*tn?ccs)_{Xuvi(k9bY3j@1r+!TEK^fe=w9xE%TJ&88j z6J7Us;^qwx#>K6wYyTmXc8S>9{TM^y5i3q4>5K!T z3xUC-y7SLF+b7|#x$+XmHYIuHp#xwO zl$aDFM;3eOY^ueYNl`e0C3Ie*a~~dEM)?qcgwB{f3ubJ&XV>B(CSD1t?TV6>QLWYB z4kHSIq#Ke?tF0|b5ryDVK6JidRtW0ssh7gXhHy6G}6Z*t;ja9_S zJf7CsssiIIJv@n52eN#M6`n7ooOw|M)zVG5p%N&0?vSNXhFWZ>v>OT+gajMC$?8(X zDszOyvbgq886udCGJ-J@URt?18<-~D5F}bl;YHL&C=-t9MaKrVB*Zgwfj8QV5}w|R z?(@BTky}6!g%)wLDswK)9e^WdyNy;CU=%P>_`=9U^1QHt5u;k&5{q!s=Hlg(5--N= zSqz~Sh;Nx_kWovS8@YIItA_|G&E0m^Mp(y2?-VHh4ch1*VE6sr)pdl4O-*Qz5j7ueh za-e#FR~9WE-C^TGuuZu^=T%B^0xM~?y6lQg?S#WN3>@Vjh7P0KGidtcFlpo{BA4{Y zahe@FVJ9pF3Z{)29;WqS%pIH25{QIviPg2j1Yj_HW0of)oy&5SvQHX#vY;i9Tv&=* zN)|1AEhAN4u~nOm~kLA#;2DS9{OqrqUKfCi z6>D}TI9)~nbK!`=5DQ;OmWhnwOvi?Crh9+ui{XvSWxa9fKhx^0fH6!1gXH z3BYb%bV{7%rpt&>x0eiU{PAc>Ij4hh0n;@{LeXUuG+A;27!{}twYf{f+1~dwNriJq zeP{ma<(`x+>t)|r_A%@#Wz{iJFB3)dl+SdSEdO}(Wxu=ox_fWBNWFZ+r9ZmvlE2$> z<@dk!#Sa{`*W(U*!cOYtJ$K&p#6312xchcW=0o?{`6b65@`jfj_2IX_?6mj1>b-A% z!AXDl5}(I@-5($R(&ruUykquOFZ&AfF^BJa&_269cBk!j+U5`b3(P*!tmvyIRjeuf zdYY*KRL!xo5JjlcQkMXz_>^xIdfe53o-c#3tMypoRP-q`Rf1|m7kWv0DsDwk3Zm43Gqq<(V8Xuk@ z(PeD1N?L-U+E#r^OilOPvwr|=c4(!+%hP17%9lmvS};|)=wOmp>b4XVHMoC9Q144J z5_2Dgd&#rNRx4X<>{clf{9Xn1vcD4L7rj)mye%}@F;wm-pq!8zaXdtbi4eAAV`@`(B0 z9$2Y)6}C!ampY;7G7?XgeLP!5TnTKH?l$q8U~UtiyZ-6-zkSuk|M0*6{kH(QD61(* zSlm;72cSc~ZGqTlIspFi2Rq$IyNipiK#&t)?MfHjp-! zNVwyF1hGvS<<7VL0RtI3215k}&>hJ|xOOdV0KUymdK|%s5JhPVDvI5y4GK&etpG0t zh#w7Byql&W`<0<`pj2N-1EDT*IS>FwE%M3`nUrL!Q4TO;WQQk)HndAd!V=Jq7cdLo z2)V~^3L+BrrfGJ;ns2QkF2cbVeSIrdGsOmPb~yWo0uNgiK`mGRP*Bx?+}i;vLxS=W zB6oVLYUw6J7%sDJFs*a>OJJFmKS>GDya0TV*e^xvw(^Du!g1kl?%sgBXSE2B_}OK} zainF($+vu1%$>BJkcPP9A(OLkjCIjsunt_f!^@{p08(5-g=>8P%AAq}t~G=wBn7iI zlnjP(r(kCEqIfO5K;6hJ?7XyvX2~~8MpTSVQ~su08fAr?AR&`EN8J(tTW`;+g=z{# zj0eA0!J%T-1fB)b&~o~^jCfMCtv}=BkV|&HAxl8GNNO<|f`;#znV4!6Fn9 zId_-0nr?aQ5u(*AhxKj8r&&;Q^k zG%t9?rnUq)5($*;1X|idYB4l%N=BJ*6a@p_DoL1bbnl}>C70$kt?ww`p9kzE98v=Xxj++sA_sduqiODt{tt3l(R;J5< z(I{PO)*-zL%Qaulo!)$5Bri%Elv%j_C18VE3B7uoPh#y1lH+L>9%ghJ1+)n_) z7-i=oiddGSSZ#pT!P@3X(UpVUxGfZth|6xrUM?d&Dqew0Pn2N!ZrE}W1)_>saVgHJ9c-X`88`m8@33iRpM>Q9R|ddbuKk(xWtw&sGb8o7BR}9gPP%BT7M@}K?5 zkxxBv`$P8M<~hggf7D@*-~G{>9<_PXPFruS$;vSW zn(|GFN3TF*sR&i;sa}*i}9mD1<@;5%1bc%C6(tvdxK1}&Y%>DOKCUT&AOjp(`= z*^?{)-KvZ>Ltqx-4h3+2W~UfxzXAx8ef?0Ij6h&Wc<~KJc{mwzq4~B&aF4-g!hLC^ zmrR>XX%`VP7bS+zeqVO+FBGuA(yl0Inj9&m%0-@qhEdsh!(wEy6XGeHq3pbbsDz0m z21Z@(4V7WwvRa=uJg@A7r?3boNgJsy_eOzPctuEQB8Ao^(zffeo0McIED1ew?m}!R zkQ{s0j<#eh;7pkXGr`lUdiwyT8xlo3>qddZS+;l}VM>Nkw$_9E*IkxiHGJ#qnC3PCBYdDM$F;@Az_oj1z1Um^AW* zY?R}?KZaShy>`H)smZ|_;wO>d8P4oQUycqy!ZTUMy0zm=5vy=(90#-HDNrsYFYV@) z!Z>UZ>ITql#msAXmMbuh@oT|EFOyv03-oZ|z!Huk!dvsC+~S$Z4hCTW7E%r@W8{ca z@@7GnX)JX^bMYX_ox%t<7bHcOaT0)Rse#>A4p#tltuCQkipaBb;ocJE+gj0^CKv%j zlIU_VI?J*~2|q3r!Waz6Snv>=t$(*ET(lh{OSRRYLvlo7Ugk19jPw=qhpJ0T#Y^Rn zqdQraFzm#^p1pFcGk-?N4F!Hf;Z-;%@5(M+M#aTQ{I%ty5?Lb!oe;VJz*;(c>FKIg z6m%gOvk%@V3}cm@9Oy=KF=WavYa6BhO$gR2wq6q<<_@CJe=T>LrJy{^ehykv9SoTd?Kb;_C(AFQ~d~-x>=F2yimCSt%TdP>^6WWUCl@DOwc@>3`tSEO}d6zFT-}uWLFFX6r>(9I8>L1>B z>v?CK`uhEM-}-5XZ2#o_x87&hZT5T2=Dl{_%1?gzNcJ-he%uL1ANYosJpJ8oIN`*< zIN>i}|D4yq;)p+a(UV?r!of!zwBzB2?DVXo_Br%`oge$CO}p*5?QT14SFx-_o&uS^ zET6}oQU#}?R26#qsa2pU*{c4OY-+SIR{ep&tD1e87Ytt8H;b!76@Y4C0?&?338@HG zU8*RJsuo$$0_alIi3-^G6_Q-49|c0r5p#+{)hHfCw3e_SSE#Be;Z?8j6n2k*wK5cf zyDp`w9Hyce9*KHcN*KTt&K?|#*gD8f*8^@a*}}_Bn^ho64s@(UHq=lW*7=gU5ityo_9)vwjZ z%IaU`EvaN?R04ax+@F_XcWY=B*IJb_m^%8xpa1xpOD~{9_VEmNO$=JPd#7`hoxu)3 zpJSUNJ-uC`o7DYCSlTNXdFfUeO}*frZJ(hmCF8W+{<<&-@reEi0z;OB=@ZLn`OAlt zUd+f#d)dvxz0JqauP*qRi^>By8y5!8;0lq~2%LUSz28YE+ul8J424Dm(Mlw+<%DEe?xSdcIRaGb;p1)DsL znuQrja$uz~EjzDbG1BGG3n5Ta0!D#*XGdF6jK0SeUg*H{=yUNJY9 zUOsw?#L8+?$^~<5~ZHWOv=XiK9E=`7_ifhvpg%_uI0HT80Fxf`XI zVQB-;yePWtv_iL+U{m->(J_U&375o-CW%WF6r*R^(_En_d-|x)Lia*mbt~cXZvuAd&-bN zi6PUGn-$ZVpheKi-wHEYC3a%CyRS{ZGNAfhNHB_e&9K#9qxY+D(| zEMsgn>LOXLRmQ=`zib;S(1fF`=do!0^8&?aYbzEoLTjtdD+_?p_}K{^jQfT%nMBex zfMi23WB3i*^2abD6X~%}!ZC<7&({yrx(-`s-USx%KMvd~?}{vG2a=yj!pR;axZV#j5@`(AsJvJS@ z*Va#X+*Xg-ZqsA8+w{c8Y(8SYU0-n2fv*zhy%LnYe&Et07Gr(QC@&>Gp(I8K_3{Z}9S&R0L-?=Sm2_TPQ$ z^o!2__Vri%^xDhL@%?3AUiQbNRLu%o;^KF~6H1mYcvZ8{S6BSH_}v0-5JOtXE!SLf z+YMJ3W!C~2A{12JN^arAhP>ff#Vtu1RS~pr=i)r$VB=isbm|G?ls|0|FaWvFoj&^p zdqyyv1o#o;;qa4u45rX5mO0D;jIxYHmjk4o$8$*^tM)}{bM#)r*a^0{&D0PEx}e#c zBMT}zHcJrz7z1OO6jyf?42JTeFA2-jP?$E%cm@{Fv}PTNxX#9P<-=Ie0wKX>TW*yE zVrOK5#I~HvG8X%Wf{mA#UeHN9o|cPF@hnN*+JcBE`qHIWp?4@4YnX3WRO{JXN)@uM z3|p4wScZHl5`)1&;tx5K;zCeO{<4B3xeuO8fp9L9S=y0c<2y#~1n1HPpi~PKZGp1q z#l8fHJNEb+)#~P`%}cn}SxUXiC4iw%VLVwH1#5y~z$h%l(2#UFHI$Ks zctzsTMm20=m6#VodS*Ja7=|tezJ<$o%`AZNbYZxDTyNcp18xCp3(;g*1SO)A(x9*d z6jA8(@;5;YBf~&3WWLtlsBRK}A;!3&0>jQ1Pt(trMJ9xjrEImiXz6om${l{@D02WG zc=oYpU;u%f_rlOkyt$WvHqHVSMfAGsTfQ)5j=Ve>@oXsWN_e}jQ>(N)pz>PEs zA99Y)_DX=NfGOG5oZ&mnLw4YI=4Vk0IP+7qKABvtw7#u;*szVQS`4Qy*qT_4N^BSc zl(V)pDC|p&Dn^%kr5|#ZM|i$m0!X5QA776MTN!^x>6RO>^KjW{g;EjoqPH^T#yBJx z!pOmLEo&wA9A_PL86mlgi?htp(kDH70!D^}3o4dKN(MuYjD)@>2bOX(F8~b4hXJ`| z)RLAWS{OWZ;kY!!s2f0A#AzpIRE)tUJx0D{M!NxBv2#av7k5*z10-GTU}KqU-Vi;P z?n-xri+|<%U)?c=+@{gp=M2-G1vz)?iq~p&EJMkgyDO>{=+ZJfnQL1v-|M%YU82pg z2?B5rIqTL7C#524SHeBi?fWUyH5Yl>l!E1AF6M51%wjBSX5IA7D02G-G1GYP7ougs z*3wagBa~JWjF(lSz_16VBab9Id~8A#&M4t)QpH{0#=vo2#EF~xnpUh0rO8dAmJ! z+Wfd}Htn^8s(I_9pSa6&kJ|Hv$L+6De*QD|BF}yL9xr(I6V%HG?z4IC$8PudUAFT3 z*sCAD{y!crE0%podkW+=ilR(msKir6s#TSBsxzgTZqJB&AX}?iRM08(6p5a7`tCEZ zu2Qiol7g-3k5ZS5>JPbR&3KR~2?d(kFc)2E^gJ3vgjys-o_;dgr?P=128NJgNUf?^ zR`9BG;j5s*_LJQZFmjjgoj|C?zds32+fe zZEL2y7-PsqGCgP(4jZqKmQE&k_-fNDz%3LoBc!#G28{W7i0pU0JUCVkS1kLsvJYwh z)oCaB^tBIW_jjh$%PMQN^I4z%n0oolkA2|e_rB#*AA67QFMs_@r}$%1U;E<89x-2e z(GM;@|9e+oe2yo}zqxzM?blz{1;4&tu5flEByI#nvF|MRPf2QFPn4y8M_I^?hfX6a<8P;xXSPKn&x7PMe6+;bVnqO?KHCNP*r z*SGBq%WRRCLkgEj;K3(V$Yw+RnmH-j{(_Drx@7CPAEKbbQUtHDAM zuCk>auQXVQA>ACC*D|Y+B_@iYHqtP)d;!9gjPx49FKr}Ev#h3Nl#j{sx1) zVW}2Z%RNW-kfPIDz^H~HuY#6;D}V)r6e)-UW=4`IZ}9m!D#bsBCc~+J86Os*J2pv+-*dRrCqDrS!*s)mXa@XFln?b zWysFR%NdUzGcV!jJI%ekS@stF7|shg!Me0eX>86g1( zu)AaBE_ldd)SXqaObO2z)yl2(F!i$6rTn#)qASP0Bl#}5tIlYS%L$sP1+-$;mb7{z z&TUJdE=9IhfHcRp$}rmIH5=DhnyCpkg_N0&ITDp`me`^a1zT;KQf)@GV69#&iJ7~- zk&7Wh;S!z>5I%moxQrW$tOCy{uhQGF#lN9&FTFKWz+Z+X7ALUte6*u+S9|K98OJ%Rg=L+h0C(UE#c@URE`$qi?(6SO4wqtM0u10?(KI($}3gp6&Va zeYc+b$&+7m{Lzm&;aPhf{p1~G7L4shW4+ylHPg z1@?GBX&LZ@Li5SW{=~Zw#SnW*#6-CcHHN2E5DEJ`SS7+b05Zjgy+j@ zw`wtU8BwFLXjNybxv6TEZ;Ca=pfXc!sCe{k+>f060VN)cvJQ!si)t|vMIxcYFed6p zJU#@j;5DQxs1lN|iju`BJT_?%#iNK?Q9R*Q)k=)+GvWPdCn@O`5s%z@+zcineu{_1 z67H!k21?|$x?I$}kl46Ln^6uJ7{L_Om}MxIT3EK(VRV6ombSTYfngUBXKSd7Ha2tN zs|zL&s?uon4DmA409+!}R$P0O?8&lX*(b48#GWa8i0rX)50pK3R!#e{uL@_Qe(y`Y z{FzUjbjthQ`iWEi@^fdr_bZ?Okb3zC-#zo<3%+yRH48CB+Mt|{Iv}|#?HG=QeIwN|ud|mDYx9U zs=2n%>b1gk>dsf&Qb=IzgkIJq#kVia9-r)XO-v{ur>Vw0g+ z>uT9s2PAc6?lCkZcSv?`1ZGDkg+PKf$M}IqdYPJ3$#XPWt3+a88T>GdB38>{3c6%m zTI4Kz*UTD8vDlL}%Q)e{Mi}KYiA^S@ham~SgmIj36FN48hAc~LA!DEuYffg}z{-J9 z=8!p3bOVRyLS}+s5k_9BMsr^?8b+H-n0YM%H%f1*5eipUkY1XO9|IeAoQ%DEU>JD0 zJTRcru6HQ`Z6w;3SG$}P$1vuMWiNmxo)0))})s|{vI&V-UNp608KaAoMBXL*k94(-nQPWw*vS{8@@DYEFmPX307 zF8pmQ1)uWWfwSqRS}H87i@ibW|9c;Cy$x9C8`FF50?U-AMACw#>EiS;G^4#rRMG z>tc3WxtmEiFk(FJpd5_MThcLV@OZ1$EMr$rmS-I5)7{CrpBVLp#- zQf8_JaOL;{xE1J!*Q{HkEM3Oh<$PPK2Ulse2rb*9H;dlc`w38sP*(2ZnR{TCnU^6m z?Z%%qS>hj@(dxV+1RIxeq_kX^DY`Jd5P5~9TQvKyRRxfbwigJX_kt{R?%Jpr*9)qp zOsZ%%l=Tc*S{8blOg;!DECDTDtP);6UvuX7)Qot1rUdY$Y)!rF0rbEA=9cAGn0u70 zI-bvC_mOP&9zFl|t}E}k=~wq|`Q?q5fB*LDf4t?YzyI~^=Y9HPfA+j*?EaGH?swb~ zk3MLxtq$2|tAqC3M!kIC?wj}DdE0$=+HTM7H}A3C*84nqtHTf2$@68O$5t;Nb?A=A zKJ77nBKz;G)#~LBfAB=c9vO2~(CI=}1F9c&>AbXRLuDfva`m#$e&dhU6Jfea8OA6b zk{1TmsA?BLeO+0Kfl*S78bBs^xcT2qThbo_KM0Qmr$vN`63 zq{RO2n_p5OS1k8M=Ki+ScfbAxT?MiS&0rRg+713Iw_K=Qq&QWwXpq6gmARU!$4h^|CyJ2;07YTjY zZ8PZ<6muk5+Gm#d-2bob#0@#o(c&B#JA9dR>Xt)N=n`v=waQouyVkLm7qQ9TaP1Xa zZn)axQ3ps2INKyCz>#aSTOWFWLI~Gh$F2NdZJ;sjH4u%Rkp~RLP68HcsJoongSR2gdVLnDwy7S z!jDmxlojUkk4h$#32JoG8xo^nOJU(9T+Fqy&1Gg(o?|Z%9w7uW8Yd+xilJtzEyHDQ zlU@mw10x=KX;5?-VMrmgn3MRyNEwFAqMZaHGOfr-iDj#hU zGH{r@N=jjMk>-1iP#;qI@wdAonR>0H%a-piYX1%}tup6-ab3n04@#sx`tN4N75 zJU7(Jp0M~w2^pG;DKK`uWwB^Jkl-F^#bDUYtEHax91AB{;;_KV>3|Y93Z`mR#lN6&}A@C;Rgillf&ivKOie*on z6$K9b4*%J83EgAm`F(7kxb6qBwHX!89yDu-dYN6NeD{qPJaEUwejNMG8-9A@72m!0 zmb1@1?R76Yey`WQ>`5-Kb5=B4MK0`N#r*N`8+)^Y@vPk91JhM4a!h^gFk%;17B+SvGS#4fk6psDImx|cK<*Aod&>ky$rrh_M*?kgQ zWxetM`txUazWm8k-gDBs-*o23PyFiVPx;yxPxck&AO7v9fA-@qUHJ3wc)on&mFM5R zJnI?C(rU$il@&VJoXbY){bI z3fz5ePos2Vi&(iCS08h@PrGxq-MMamiqY^klaY?Wv%N$TGcaKp!7%!NQsI&pDoILJ zEHj!eeWWx3m@<)G8A{1vn5AusqO60w_*ehnr)gBp3TiyE0x(|yM@nx2>bstniY^mv z43yHq%u9<6=hWSdlVmBuQ(EwHtBZ;>wV5C=9jzVJ;am{c0k-&%|F^UORoEjCK zB@%jjFZb4SmK2rs!4qJ^mX{DAOUWM2f;@h$xLPYCv=#G@2Jqq{w;&RulO+;3HdGWX zOYa?%FpHEVz|xc%$;Ia}|cXB6r)8SuadORQY9P{$YdBKT+VnH z@svq%SqPV6&FFHWX01ez&7u{z&=WEKR^AmNJy~J42n2aZp>cw7X9U1JqRS{mco<%} z@S=;tm!23Rz|%E4^AcCihFF$MB#N#i-3#1Bhhd>}r!>JrBX8nhr8lus*0)W$wu>AI za)jNpc!;Y`mk|a@vx5{!D-#xq3*IxScw~aMJX;C?5_uV-($xyV3qIzIWgA2DTHhMQ ziH&=43z09q`~mWX2a~5*QuWDjZ3zI(5S=4;N^|5`CMnM9PWx2>%3k8+@AOV_?01G& z-#PYI33jUQ$CIlf(|u2K%AYj~mkp&tN!&&$)eF@QjAN<1QhijClyKlG5jt>q7TLbMFdaY=T<4)v&PLZrZNx z7FI-=bYC|qadnAIdWVH+i;(RyJxIa}pw?)&`M{d#z?z^@(%h}EC8c91$`Z_26ibac zce?GaWGtO-Q)m`iTT|Oq3a1+oujv-k|Q?=1T|GbR1jtIxgj z`hWP{J(u5e)A@JbbnbPRe*4ZFe|W~JuX)7@2fXEVN5B3RPyM4~_df3M#~!x-j{84$ z>x1_6aCz$kcisM=$8Nj-qgOtUeb|1RUwr%%-uk*{e)v5vd)phH`Ia|4>(5{P%x537 zk9zs}&wAp^UhuRXcid6Q>$RLTr!5n4$s2gG|~2eSO_bCDqo{L@@~8|MtIl-OE3A@;ko!jn91RtDpG7XHNRb_rG|_FTZ!?#Xq_J$_wtj-CslV zx0P@B=lkzg7rWHm1&Uv-E8o8zk?xD-9Z|h(yKwC6VC>$nI%|t?|JoK91K7LTSloBr zYb-h14Q4+vI*yKNZb_pogbvm|6I)wQI|Fh?FvgI9b1!0>TNb16kY!6*>ZL6KLPuV# z=>iwX9MR&J2AD$99~DxdW#lVLag7o>3m!@tVqn+mD(>jW1LJJ(s`6zl5MNRPW!|KW z!rM#Xu@QkRvSl$%$kvCwa81h4at);?P(u{Rz`Q0_DlpKxXFKv1Z0Hr$Q z*-MPky3t{XA$n6-=D*Ahz9cP+mw-^lg}W|HVuHXdR>t}e78vPqWHefaqEjSb3cV1z z;oSl?(9f}}5chC~YB>H`K-A;$>7`AeDdguz24_yMLFS?enn2S}_ z>bfbwTm#mV(yL?C;sB7DzIjC`6Q?z&IGNKstV%%W4uOmvSt-Q9&wRB;9sl?`6L!GU znygt6N=8x0n$V&@oIrXx%DnZ5WYl_bwTy~Eld=>JHqF8-(#4<~zUvJY>8fL2dEr*S zdD>h|p(N|pHrdu;<;1?ab?|K&(TZymv&dpuSUE7#k*{1Hi#l$(ZXNhn2p@Wo*D)KW zA<9_17IY$|$%ilTy0DjCql_*pRZo{zx);=|!xz;E%LpS!3>Kjmx-`6?37)0Ez*pcv z$^khx7HyUi24m5dWcjzY06B&!VKA8Rf$Jg(Q2s+0UXnPml_W)mT*`AQL9hU8A|e5n zyn?Y3W{~i7K>@UqVW~il993)pc)TKRTSM~EB~6ynB8QKVOZiaZHwxLrIWp3*5%$4H zvc3&(`oPjQ@sbyALjfar%BN(wtY^;BB1=XHGF`K#!^pN9zl1{?5~UExiEeY$mOxW0 z%8bS@5dH+YE-y$fOHRq_8k)q48;p)&g04}FW)XTBePxSSeJ9nnySDkL?8v;!w58Y_ zkd$*1*OijQcs+MEEZ?DN*pG~?77Y7^GdkpMJDql*{LK@q@Vz!Av5g*t;d{8%3uuGoM7 zJ3qD~3I>_qHQ!&ZKQYkGK8XZ|?lp2k!XIJvaU1 z?bqIW>y`K2c9rMLtB;tK%olCB<~$#Kz3J+o-Fp4`w_SVTwHKdz?S((_Czx-#{6|;* z@|#y&@Rid~e&x%bv*(F#I`&n6wD0qdeAIKEy5o^g*hCK4Wz!RO*>vD;TOGQ`_D|k( z2alNj_Lsu>sr&5ozBj$(j1Ru?lkb21+h241tN-XJuX_GrFF1PNC-1e*;~%$qhwV0P zv(+ZQh`o86OT{LwMjNV+aFuPxkDqF@2e{miL#VMGSxC=zr!XQy2 z)6K$^{VRc#_`1AEIafhrW>GRTa>UaFxgcyhYGE)lk`Fr!Fp;5flw6FOpvTR+ zU;DDSKFjSnwI3VzRNddO_ORQ(9;oF%PUtLZ=g)uk6EI)>{F&s`5B%jj-~8G$PCM~$ zzW(X&edp6Z_}kC@=zE{P@ci#y@{8|Zb;-}}*y2a(uDNr|wZFY@<(tUrWd*Wp+eNNb zGP~?6oGA$aOV5klrw&4HJzGt?sC(88?tYxz)mb{27R)^xIK2C}o43sh8wnp6c`kM7 zGis>NxeQY1_B~)m{Cx9bA@CIi>|~7|Puo#m@J&a~$>b1QJccU4DD2vCFGH4M>E;3u z2END20>LUY_ zjD*V+p7LBX$MB3L07+l2XA@_^A9J*|Wr)@gKy#!UU4NYviwi;9qG@N%b2lUhc2c(C z>Cy|2ADCmY=(8;GmoOMMQa;)Z1y2AZ#X^f=1b-m_dh^Pn9ZU+xrh_L8dnpMKh5{xl zR4mztS+}>*dD}IVWI+p)#EeQ%Qcz%uE^5@xqnFkKb8q&1-o zl_*X)&ToP`DT&)rM<(XQ0^pcLD8vNm&82KRUo{Z`-MpGb5ht$Pq*QvPw5A0B?gV9M zrqAdlZXGI<-F+sADrrr)Fj|l%7@mb-Fggn>Npo?)Yevmc`(WCuFQuU}*G7v$IIp|? zYUS1cKYt(yW*GQdNkSoWwr?4Og=y)Qj_kT)0SFnP*Q*0LJB*fn>Q*{{c^Pu0>27!q zM_63*ZOY3KYg`Z-Wh^dzyq5g*p-rzlfy$O~yeQ)ZW@tGoVt|1K(5{pQtC>>>W+G27 zD;^e?v~J{iVaozQbYTeaGElGqqeY^$DasJ$&WPM=Dj5nRnQ%tSTpOVT7zrhCgr%fq z=PoEhL&1h*Lj|$~5XOvQL*y6>Jt>JYLu(X6I9sKX(dvpQTi~*_>?Pa_K{slsKwRiE z$I^?Lo;1oa(WS)JYUEX0=2~4a$)rGXl$9Y$NyfaU<&}jaNoQQSg;)P!&3RTqs&pwij_e7urzcN7BXFXd&u6zv%_0RWTf5-I~-*mo1m*i;zy>{t~EtV&hlRoLP&%5jQ5ztg1}C6(UlM71GDMI|UkQ=R0BnnTDgMWVRkCBK7CUZj)Gd1<8E|CdGqhLr_xWLOtP_f`;E(b$k zrqdFkYSvPMmqU3{EL!WsMH*ZH=uM5XTIa6R%ULJ{t>uMmtuRO|!PzWyxzky!kHpl) z$yd)((fg{nuUGrn{HMPC4WFMs<1?T7s7m<@pY!Pdlb$jws1?g+efEsgKJ=~+o%oi| zo^jGQ&N^Mae8JDY_2a+)f_nMl3;zC!i_W?2#!K(qaCVpC-RGWl|I+KSvp8mF2X@j<2TNJKNmuS%H!=%An090BhQfVAK?2`4 z0zlDaB&8|@C0!txHjUUuU)d@*JU_1%CLEmxiK*e|WxuMLXd>7IYJ!E+GQ#9bZ(d`h zND@y9q?3F)Uw?dx^we0l5>KqBgbWo%`d zTJ8gg$EMAZUOoklhgWfDWulF_xHux>ml|%S+a4 z*ZMy=k%uZ}2_TV7`S^@Yo0U}O!lU!$Xzs`{_Y&)>XXyf2Ix8lSMFy~*@QfCj1x5>2 z4zU3+Ca8ERShOs~qF2yyZ`tGYBGz5s3MU8rNM@A2T!i8_8gt-uzDQyyrLK+aynqQ3 z!br?(cp4hQrpyaf5<~7$)lNQf1`bBIlI)@o#-^jCb7Vv!7NNlBSZ-Qf_K=Mt@*rKLksFi?z^?GVP3OE5zS<3!6i{J=z4w!^bV;bK&V%_w(y#<{+YQwy1_ zAzagibZPlvXHVFhok{ig!XRUiLlJdbLzXoH+`QN+`67o;!cW3w2)5)|GK4hR^o6Fk z4wS+@Trx|}HM-}UZfE<3o4q}xyPn=QYqTB4CIc+)#F}t&lT}?hfa#E4_weQ4Xvwe1 zr0)8~;ZDqyR~MMw%syJOYO5^|dSKqQ(sh72M7G?Fy)92L>pjIHL3n)EGWA9m;l7Xd9tFGwx zt%zdz{yVQ%EZ=j>RqEw`y6ZY$RrYn}dv3kt?prRt`<9E8%>Q)vmB0J-b$8xy$t_p^ zQoZaCG2e34**9MPy=yP}=EqNZ`7@ug-5XzV=!=eh%rS>-_4FrhI_99Q{`7PkhZnp9Vn z%2!>9P-#rjvLmS^WLFPjV<7;14XHg#4;w*JGQx@OVKIONq$edGtM+^utQ=SX{Ap&i zRh_zp2qUO+RlO`k;gHX7W#UM=>05Bjf^!)oMjecfNh`U?5(xBZ)gr_!JHOj*h7v)Mgp z_AvVIzWL>^ec{s|`|!Iz^1*j}{>+ol`rJpqe%5K{{`f0D{oz?YkA2y%e%SNnyKlMH z=dpkPz{*$kUGsfg*#-aK|M}Pcv1m=P4AAZ1UyYo{R!+sM|G?;ut7{8&54sas=$*tU zZtV_KETb^Gg|%(GPI?4@g}XLzmVrf!Bs>M65Hs{}#zJ#+j0A4Gpe%J00T9kCmKz#O zK>@f=s_I-8qisQEM4y~XTNfUzx}(`)V^QoFid;S-axnzc;&?_(I7SQ4%cxBcxNz`N zN(>={hvb42rsy0=Fcw|~nzhyCqKkx*UblP-$Fg1wwE~9l;5X~xp%-piLvhZ!Wyx{) z<-k`9CIpP3Wp0cBy&{In5XnrlnMT5~+>Evqx{Mr$c{0oetdw*OX@#)(li_*=K(^fo z^yby;wKQ@R+9=Gpbjbl1sF#>da5{%jntpB5kt8-lO|}_nb&Fnzk||-Mh1SyJ(ZWO( zz-Y9(DV7dw>Ar?XGA_R=oz7{U+#EI-of#Ot%~u>bOBgkzJ5@&K3Kn$@Jq*~AXJ%UT zrbf3)ax+ROO`KiY{iBLm5W2cjWoKx~(2D}#lvOyNDOCc?ydxaD#3~j{q{ENQOOGAc zFFW0J+s$;RDF>%H0gl2rLFYvg)WW(#HWWj03k=x=DR^8YW=b5Tz-$+glmsS6w2&?# zFbc=k#13XP9QmxX6Q(t%$ec*n)S6}!H1wdXKE59wFS-PTXtNT!2@2i3xN~d`=SZnb zUs;XSGhED;d|rhSKF^2&^bve4-c+g*cOUEQgT{SV-U{p@?0FMy){U z!OZL4Yb{S+>;zjb@a1VD>=?MWl5{S`&8wh{*{it>WigujBm;oF7bT1~k_d~a&6vA3 zMM~_m+Gt6}Fh+GJoKy)5p%@)PIq0?^!j+QTkh2p?)1gR`<19k>R;jo!jdCFwr-e_z zG6F}A*)2B6H43ki(bA1BEi*KVlu7`9u_mtWdf^D)1%_NQFuHU~B$VmKRyg36R|`NU zH%hSrK<2_8vbo2crEu|2JM;SgIJ@(A%c|=B_eH1JE7%7Sus2|X3L>pYBPwEVNHl6< z-$%g)u{TDe(Igs;Eh&8zFx za_*wbnFXg*MX5VUT4Y0vLemLFmvNanOOEWbjDkwTin%f;x>i+UgrdvdC(~u#1-v%` zUasL?c2HTC}?lLQ%byuFdUagneSqRJRxA++5)jO+~ zmBtNqb4?MUYWAs5-;zY`ym^ay*~8ekY`whavDM2yuzce+=Wp9`uE(%fp2WWDM>kx3 zuKUX#$yP7>RjDg3`0C{seEFgueCE$jJ>=MD?e(WGeaiEn^RVX}crW*tJ&b+q!4G=D zVUIfY-~*oh#D_oi(GNUupZh)GVfWncfxGVefDH%iz2W#n9`nXm9{b_9zf#Hkr>8vU zlwxg-SLxno^G9{<0cY9jC@*QN97xWr1f@Ir3E=s+2u2 z%*)+UcJ*CUF(?UPs%ni2T1l&fEF3%?n;|8g^Qd!MjK9~_Imce2c z1zv&4qD;m^Jg^-Jj3_ElR50SoQzmjo$k(V4z;f^mxfW7ZT*Lx1iiFN6c{%_`(?uaB-(0Zp%X5Q*a+V;w5qz07JvJ_em~LW!{#e@kQK-)pHTj?1Lx@Y)VYX0 z_5A)1o&L@_Q2`-%5`_LJ{+i}~AM|ICli`AYTj zjn`hPUcULdD}8=>XZ5lgIqBa=N@++RNpPML*A}Oti;U`w=sFEMmpk8U0lF4BwPBWG z zEN(}KB2|{9V2FfolBKpOl(2|ori^VVf{J34aV9mBjX}3ql4QYVt(J>CGXg8LR#p;j zRRJOyfFYJfLu$chvSKag!rp3Vml!-HKSn%dTP$tsMrrXy*_q7L=EdO1dT%mW7G5z6 zQItZk=Lplhbj_}~6eOmTOW+bP#902?FucsA=>rVD(2fy6?pd@&k4-C$5@yHGLLt0# zxr;Rx#(3_suYP>5>cgJF9>XS~`q%D7byI=UCd4kit&y%#+hC3()u-+7+vKlMZGSvK z(4|Pjlsm*SY2u7@F4~SrNzUlZf~8rs%M#9xtDD8`cH~wLQXP?xB(qCf$=rR70Z?YM zl(Az}0!43OI~{gx;eo@n_@+RStr>N6?5WJu+`UekrfxGTkgoj_={f^hv20~zWfrE; zvQs2%bJ+IP5GnJ!CbC*D%M&wqLP3krOUnyGVr5HNne6uaw%pr)QV`xsq0*T*HFU4|(LEeh>Y7wH_+30JD zu%g#m3JbzZSK5vjC1x$*j*!rsc*e5Iq7|JZksC3Z3?Kz8C8QSr-PFrX3TzyURc<@B zJFAy5tSh2p?uaxm{PGVx(U;xwH@on?-a=+%rv&Q&iBhU#H%hO|S41nXPLC&F?Ih5W zc4V>TsEu~VHh#!S;4YWN^VoASacX;0>wRtBDLU)dOuUF_*(X)T$)vcgiEJsw4ZLGJ zGhHfN%vThz%()<7^IDY^aaANzm9vW#>B`xV*3?|AT)vQyawxaf3cqC7127sjDGH;Z znYb<^JZaX;R4rZ0D4&wK`+B+Ov1J~t>9~^1)>S=*uKCTy1-YXW-b}+_XqsA<-V0V2 zWaTkzqF$~p{@Lv|58HnK8&$mjFx+Ll<5u4RvSr(Kmn#H3Xj*ApY0z`0z8t&T%{{6L zK=BmoE!SUh+qNs!%l~xCwd9VQuKLM!m)@}DLf`jw$IX}Ddc!5Fx0rq9*LjNNZClQ{ z{_^wOV!q*ub8or!f^Aovb=%e--gxCV-uKqyk9g{RU-|sU9e2py&wJKhCmiySL!WSu zBM!XJVNbm8bN=w5hducrYGLg```&B62W@!t12;VWVY?o2(8E26{n2-ydfJ;_;^y-4 zhwKk?$P@PZpFWSRh*O#==#+%2KLwpaRduF*^x(95O}SSA2&QmT-04!(hG0h3%SutT zo+?l6+2hR>eUWns14b7`!Ki}fi=SYw@HFa!$$U$rQd%J?P~V{~C@&)6VCBOCre5}2 zSE5wH%acw#N?lO(FQd@9706Y!sDz+5BWwa;7E~ssga<&Euhx9Qs%k5Tbs6KA3?rcs zc1RIToUdiUFDsnQT^FoH0MM1H7E($$Q8^sC|M2(zWudHgJN|Uy8Em@euDjjrjSeXFI{Hh0^-0fTzw=Fh@|M@X>f;}N#}_~Qp)Y*){eS;AXZZB?Ip6vVPs zx5rDdn1BcHA4+a2U zbeTwuz_8eGG8&DR(NI?{UWO7nFpQlf3uZjPEVh?!a%oXzhpd*#tBJP)e3hka zB@HgoGSXQnhA^junXF)TCSkAiAT?%j)XH!U6@L*`PwVF{~03src zwyq3Kq0!uzxtSNFD?gr)THxh^ls_1;dW2UCQu5ldbId5o$rB-8T|@Y1HOg?=f=LgC zlD+v_3K=$w!9=(W$HJGSL`5gOxMz=imX0D;fhIi|dg~A#rUhhochw58G_|}Ugwe%j zsI!ut1-SQIc3o*BpBxwyFk**bM^Oo5!(dZq|6t!@_hK{SKmUXRFdJ`sB-?deGPRG} zyX|~JYBAV{h(1ds)mbQtp^hd%M@nnkAbFBvK^W~8%62kmG=xNY0+H@7OR2q_3>oQi zNM_^I2Fa*|RoSX$?cjqANykMX30T9{xfjVS=~5XaV@K8@Na=9MT)j-k+%Bk-0zcSD zJYX!n@Bmi!UE9CMOv3VaXryZUSK3;WH9^v@Z&s`vdU?p-f9hP&tiM1~ZJ$QLj~QoF z*eJRJSuvbmB&v@hLh8cgC1`t8_;S!L7r!JMqVq!AtB8WFQWT;MDcfFXs`MG~yZ z(hbe%au(K#^zx9TvJd?K2b5I;CE6=Own1^SLQt|-3mM8`4iqR$4rKZcKbH5Y3OAQQr zmr#LF$tsCftQLf^Ojv~IRgtcn=n6dy>ndnTQ*M3y>j0YzFQFL4y) zCErBmYF6^DU3{B+6PXas>rSFV*UBv=SZ$W@W2{{BoEJW{F9)JYAv#P?oxc zmzP;yU^pEqIyUcj@RW3;&5<&rLj3OEe7U;c6cf(-k)6iM-DJvt{O*_R9>%U@Rz%-& zv)^jj-1mA_HCHKD8uVj2N@j{`LMcsAFaPwq%WvJf**AUp2=lM**rsH5e|hVb7x=;D z)!&f1=%-sR^5JFTsccpAwHwd5`Ra>r+i` zzVGY(Z#?CXUwFt#&v~4Z`T2)E_J3M0tKd{*N;!2aMO~&!rKp+}X!8ME#h_|VeW$Eb zEGtMWkd=G73OYPzeDHl*FkXB;iS3TFs*^9FkSPyUi@KtSt1#pxv`W^`RHAyM6v>PkhrtQZx`mDTbQ5fYV@Dv)IbCN4}#Vh}W{%2gz$Mz{-ySwzu| zx=F1S(a;pmx)?}P5Kd46X$>Xctc9aXz}oUPwT9?myfB~@J&Oc#k-Q-!Tr4jyH<1;^ z-AeXgwht*+GSlb&@<%@O9>$QLJmWO?mrsB98{ho8SAO{YZ&EM+?H4}e{_@{_@xwnn z>x=5;O&5LN{bjG@>o#9{%eJdMN6`}$P64lctuk2!?OdRE*(dbQ4yT)|iVLQ*l465o zpEgFPQjYVuNuJpxDwo@c~=`5DL*g*--ox07l*M<<73lXp|B& z7(#9CIHLs{y~J2xMxtU&CJ=I%Xz50CXPi{o2@Fd$n$cj4_Ljbw%O{kT%+kgTqxLkU ztA&Jb3M|#jx7j3g?G z=#1gJjI)q&F@#^jU}K-r`0if{vI5|kFO05H-8ktiVzD&DuB+AU z#H0+Kj$gejE3i(q=EbEmVrGh*lv|_lS+pz!b3%_0ru~L3N86tELX5UycF{Z5%PJ0H zkKNX8+-J+pV%rl2TQik|&|2MAk76#Z8@qg33bmx|fs=x8fidcz_Dx}6!zM!!1uEO( z(kap?$@XyWhz)wXZ6>ubLkgKz}7ZWd>amzLxSU^!&?^KFjWy2F3h z5m^Ptj5dK`K|bB$W;Y}dccZbTl#CfsCRWPQGA>dod2L>X%9cXLw^drRR**9V>nQ{P zqhxH?)l&s!j6XJAJO8Sx3M=@$;0IX(<-iWWB~JPvu}ze!18kCk;oE3bU;x^rtb}7E zYr+{FfV9`7HzYeMz&I;ya4yr>5j>W{O~lEn+?K5s05eB{EV44KB;v8Wu=h_J&x@w1>I}hN@8{70U`_k6{zlvD!Ifm2wrd=ds;lRx%6tw%WT-0}v zMh($9qEZ#&;i${5V&$b2#m|Ki5{CV~?|Lg*{95%gY@LIagn~kzE4pjIkj{MRHH+PIT@8-~&+d z)ioqr;i|-46&cN_5QZ?&ajwZz=#qzzaMlwzmn_8%&n13Clwrt)mwX6?m(eE6A`Quu zPXKF3!Na#9x+EK=7k6^QSwutd+$k+GhC+x!(KXZ&p_H38yB4O16#PgWonR~_&rT5~ zfkYQihoDhiUhujwbhAJ*MniNk z|EQsK8d9t<9)v6~Eh$GvLr4q_ZBeA{{TFhCePG>MZU3NV(bcw>vkkMKA??RjHZ!XQ zj@>mtJ0FU!J&)0LDK;BmNP?N*EIwUsLYs(I8l=NC*BO{5Zpgg)_tf3g%R-mT=Ns<&dqhTnrwQ*`{v+ZQWsS6eAV752o-ZV{(n5^UF z|8I17GBCc57r-ow(H4sR-+#OEyTB4NHLY!41(7M67iqiLUa_4jLR1(mI32Tzh)Wl= zmjPy?>lI?+7Q_lf)dmtXyHz3sC|qEA%P1?#c(uGN<4(`f5H@&i0BPH`O4|@Sm!&(4 z!(pc8P6`3T9tm*F+7ZTtqbNccg4e~?Tv*s!E_$4l9C?)@f1Ba?v{nXXm#0Jw35+sDLr-yLF?twmt4UQGEV$PMVxYy zozWtcExovuBnHM7N{Fb+c1BB2bYf`m!?Q-g!xyKPiGSM3yRZY~UjAUFmZi4$3p%M@ z7J@NLRJ<^&l&ghr`>8M7-l{tGRoHGWdn8-eXO<~Hzr)7TGTIef*FClFrp-;XdHPhz ztXNPfP!tP_3*V3JC#5`w?H03QncR7^4>E7^Ft#6J_G41yC)clh<(JPdZ`pX3FZ;Ul z#?5zZz0B`P-E`Fte)IE9Kfmcm?|IX)zVGYhCp_i_M?B(`=kEKWBmTfgn2&qr-Y+@& zF()7WxMQFB$Y($KfvV=G?SF4iVtXWe|GhS-mydb+J|4;T71{56?F(M_k|R$!<{%$x z{-4&%m5NF;6`ATxomh#=@}@s~t&i?1a;q8LEUp+-4=N9VRdDJ)3`9lAS8=CQBy=?( zUl!%IighYtMX?Y{KRgOX-x6jBlBi_eU&g6sEDb}t5@2WKOGlWyA$h{6G`X{5fT5I_ z!c%DJDNAMOS#d}fc3#!ICCrYWQRYoe{)SMU{=RpKuGB>$I`{X#_Z@sy-@5Xw3g*t2 z(JDy;42GB{5-TPIp^Pq^+%T|+B|}{pE?Ud0YBow2x|-T|dHHQ+A71vXwW`_uWkvBf zzUCLT+>T#)LYv}AY=tw7o6djz>5u;H=RfUv?6?2%wJgXVczAV8_-;4WJB1=)4BFV&g>~B)Is;=Uj8uCWMS# zA1L;{6P>cuA?4GaU$zuoM&hEuS63Q_1Tre9Aqq)s zh~pUPvpzAHLbEWmL~;xudW1y}!x-~qXcUj>05F=4NmX1MLIv;YO5%YGY%Wp)Hg`G_ z$d-#8n2`l1jKjfFs0{`Yi4KlnwM9RQJGjS znRl$0D_|7H>zVM92AHg1=pj+j38gf!!F1?Z0+>i|sTcr^He^%?qZEWjCjoZG($lgB zSjGxmh^8ZFY+=|%(FH3d-6jQ0N85=5-|Nb#ve90}ro}$N=D}9M`P}ZM9mSe*&k zZtav{%BQ-%{Z%`+nJ+w^HnAKjx{R%A$3hNs$Rv{hn3q&d$Lunz&c8 zHhlA)nT8o!j!l-5?v-h4EjKbtEl>Nprf<}g&DYM*EMU0EN(khbO(c<9i5UWLAbQzz zC(C7_<7qKx(WYAkH~UZEGND9eh((4g$!!0YYH+9dZq$<4PT-qKmUven9MDG1kA?NwE}~T_E$)ZDA?8 zj1(|N4BG6)opozo#bT_R<1AnkX-MWXy2P{VT0j%w2;5>)BG_2JB!Orp1mC`Gnt4f&2-1MYp(=G zx+crP<+L|4LzdKfdatJ>W!~W!-yN0a1vY_Zf-$f=lw$SnqY!pmS()s4Y#&%A6kRp4 zXRwvbYUE03UB;ffRw<~F6$?FmN~u8Bt%9h0p#Sr2+ekkq-A{@G7A-G0-h>gCHX`lcU~^7-YT-LUEAE$7~`?E=5Qyz$)6zxU10d*%~%KWVjC zc8mGNM?LDOr`+#pkKX0b$M5;_6CQucu?HM~=pzq((tV$Hz&#J#ceeu`yy2mHdQSVE zM;!D}-;CY;Wj`bOR0#C=o4nrZL_z1H?RUlv%FkN;T@)DgQ zAHIdwr67R>sCqRKf&tQWjH=2_4N`$zy)Oi2bID5qPPmJu<%p0=3n&vwBIJcxi&p6J zG*gN$M+z5#bOp8MyntcYDXL&4vqIR{V*92qA6r%(|KmTHk!QL6%=F6lX1meMNZe)q z$Z7BX)JIPD60Er+;+Tm(M@@>svORyY(us&yBa; zc{S1;SY**sSxbV|C&=(dfgcU|q2)a@!dJ3|%>!0ZGpX;Y_l1ps#4 zD1_Fn(#tQqaLeY)-1P(QiV?mjFeJ<9kq<2em{D{gTsmZ8BJ5^SGF^PY*i}`P((q*R zj|YZve4A7-#u0XsuNgI|VC+!^4oR1Zd}LCE!gokGmM}}YmM$4CS%i?b7LqSQZSyLP zF3!9Jf~m5Ofw2tP=_SU?lH$x=lq3L0b~+`Y1I*z-!rt=sYDr(V*(C!h+3EMk=;%N?1YB&B`evABrwvw=Xj!<- zF@>Sqb+=iSJf%-Y_f3mGOa z#)c~N3{8T43xaM z$f|B$wc#h+8m0Qn>Fi3<+ea5Dr$1v?gXIOGWx?E(DP=3PZ0Qmh(e1TS%VqG(^%*at zvNbe#U2Fh`lBQF1u;sA z)y>yl;^wmFvA14zj<3kR^@fYp%Wg4y9(&6rXZwomJGX6AFaOic7yCUapI`p?=`Zv7 zTPJE_EvR`=kfd}mOpa`$NhfiHaK zV}5+u_hb9MuTOvUJ&NVCzxxHhD&_Obz90LtAD?x-FQB`6_qRZ0qahW*b6#V7p=%UQYJaGkv9G04{o7 zqf;;xE@oXuBtjs`h{`p@1*A(5#SpLDVN#+RHg=YHS^?cK@L-16NwZ@yYG^7Z7DKZF z!L*Y`aVO$1U?vhMfA*wO7e86%-0E>Dy;e`RX;wApii<~g2n%5-X@Zy&FaSgN=`!yM z%!s7tnR9GX_(g2K^c}?|9g=i0Z+7q*wb)9j426@4qgixmcpg=y4923{f=q5z(BcZz zQ7(s)+_5c6v!)9z2QGY(WAn-sT@EbjY%cbq zFzx?YN}zCZrU+q1<=@dkp51gT3oq^!EOg-ryJaav3ofE#yo zaBM3J5Kp7E;cMgHk&Qbr_W%^#I63O(qAi3qF&e&u+iQ4WDY0qWU^nZGc`b8W>JwT+_)cdRoTPKV%Sh)t3$ZdtB9}Hrm$B*eT4Ir5um9dDq`@Mq z!0=r^$G)5$+Ky4nl9xb9(>HV%Y<06yhAv7J*Q+j`E>^CJrlTu!*Txmue%)KcgRMXm zmq!;#nM-35Tca#9L?yQXa#&X;0m;aUP+Y6adD3EUWneNWA&v^P;q+4&;a@U=;)e7QQx8LyV zpWXZ~ci!NezT9Qjy1BgVDi2^^v~A1z*Ise9lG*dv>gC&Ryu`0C`~0%|%bv&feP2Jj z@xrSw{RiKV{n;~5^?On;Jn9epo)mfI@sBtFhu_q_h(KY8KduR7t7H@))scl^l z23%Q95sL)|KX(gdnr2iWvzD{2=@d>EiJ(>+vf^4S-42cFxO>WOD65x!NLgL%$Cv4D zI{T(CA6`~dySePaY~ud1lKG9VeYwvspZS5eDw%z9`5RyTq}KD;S6=pgHN&h%yrFi!Ljq~%0pk-BW*9TD7sGeHfe!*Ie!_u2t+cM zVi__3!bLa$dTeFIE{dy4 z?$e5%AfyB~s@n?K!&BsJ3Qdy^07;p(;tOD2j6}B(6kXA0NwafA%Lp)1ToKKSP{yl# zbXx}sK@xc__wWTZYE4RpP5^XeZX!j8Z{2i_a@36$Y&l0$NV3nBeC(Ku)pP(5lJMmZ z5Pk^`#;cS>kD+|LL13UujKCO0qP(Sx;+R@?N`ZzAtSGhFr)?CEHyuW7{wJU`Q>0sA z_BA}EEd)iZcGxH6AVL5U93NG6TyB*u1#LjS~PF(#=*^aG!1weNAx-n?k?N8c+fw{jd zt~}+_>#(joXDND#0<`AKS280c7emXrvy9l)*xrs67R+dX*PNhyanNS#UHsmyElnVw&&|bZ@ZTHx6 z86qv3Q5d6jwT1*`lt4&rc}^?IV2~8<@q`~31t3gAYXDOj6&(+-v8YN^UY<))*0>W?rPZ;!wK#JGUt1vE;uYs zd6^)=UxpOj<&2t?+`KsFokF+OXmNBqj@((2axTL$4)e^t1O_0hyAoTy!P-TM#pO%R zu4e5Jx-c?w6sTCzXcm%@Z7Eq&=d#O}Q9=JJ(DppDfVH+5W)>?evv|9=l3lA?J<$oH zC6!fg1iDE`hoB=e_BD!8mZt0?j8H_D$hB60-N(BUPhb0jFOOks=L^16(CXzxnOvoO zS9kS$z5KQ;>9fp=Wxq44I`HjYKIW;iP%^l)>`wH`!`nZ-&i&%=`Hq_}Q!o3LY{l|Vuf6p4>o4_vUq8L(M}B|#x{Y7`)Q4XA z+-E-MMMv-B^ULaG-;e#m!yozFr|os(ArJSt=I0%<_lu5x)C-RMgX0c;$l(V)@SsQU zy5C+K_PPItr|x&3KRW5)cf97L*SzS6Q%^eVov(S(2j2Xu!w-A%e*5lq&pmc`e_5%# z+pZgSS^3*zd&#`^{<5M>5vLX--Pu%~DfB!St*ana!70`hn2NaSKE+@)pdmG&au0*b z4orFIPOIWk^=DKuTa`zt`h!>7YE_6@QG7ZW200Q`)vdas3kqC0*+-Ar<;KpJC{YV4 zE)qG&Q1l;u|J#O8C5)1xfA2g0C=-tGx>`e8*)obnH+^6s2!-WGKlqN;$F~icIIz3e zS^&xLvPz0{5s^cZzLyLq1}=uw=8(;cox9KyRvpXS9I;6W9*M5$TM)A=ACAPRFUPJB zRw;WV+ozb-%Wf#Mdl>tpXZ(dvE~}~4%M|yTKk?!B|J5hXP%QWR%b)q!dwp`*E#|-c z#Jj%tt+NHFw>q6QVuG5x9m!q!JpF$XI9U)M<0blZ$=#s zZR?N`4@^?T+>5)MpoEFh4o6w#A}%SSwuNGWY_%K3nG_;GFNWCQb!QZLiLtMd@EC|M z3E@S7$KStkWZr0YL&Jujd#kaGTejs>3&U6rG1SKFZO$k*LD^eaj^W1(xEWb+_|hmk z-Ow^hUS;ClbKYKaESCQXc>S25R%5Zc_Iot#G^S-=SGu9uZ!lGB zYt_Ye9q{T>>sGfrzdR;-0$Pk=|p*y7LYQ(PM%5$%Yo zdBtV1wG+MR7^Sx(@`KB=m1JOcN*k)d*f`~4sDJudTbT$gjgCu`vVdOCt|47UOsd%d zcVxBJyjd8Mc6K~;_KxYaA+=~B!2})H%INm(T${S~pZ0aFfQd*xqeak_QatUuv_r^F za58#0oHz=t9$!4Op7>i#K|8XgF?-$Ywd?Lk2aqGe4xB)Ad-4I?4aqRWENc^-(E`{K zBSdRKI^qpgeE{$^I-)zY6ut>xjx6RV7Kx!)>$c4WzGcw`Q-V!s%(1Dn)=V?7HK!N~ z$uVDE>$=IlHMNH8m&%G#I zkZ9p~)otS1dBF^(nZ`*kiWdMRUkr@eEQ~qEUkIZ+iqpA^hRe%;T_hLJXw6%U%aM1G zqM$9q2`!u`Mw0**XYv=ioEcpxT96jU(BR7uGQqY)Mjq8&nAOs_i`7bc+n-7HeP8t6 zzqBr5X1CJrq+aegYF)U9WrTF3?uxQKpXma$)FwD{Tp3+mMuDs!@Wo^EJn8+4dkniT`0D<0c0)<^vbvy$v6ab{ z43yve@-{|ymfc@gP*gJXl}ElD+vk`6^=CIuz3h2xKPjbN_W5NWUcU1un|yv*z3k?) z-;?rVQtIW~uDj%#OTT;bma{jV|0U03f98x=Kj)x(pM1=td^xs9vVGsz^PaKSF;9Ka zOOD?E;Ky$`?4Ui4dFFlHWq$E<_w`xkW1g}1Q})|q-v@5+NcIblc-)&`ecbC`dem!A zKI(0!p7gHQzU0}5JmGPVdDz|$+T%WZ?y|@3cYS_&_ucl~S-tGGsawgaHljcy6m?nm zmsNsFLiL@ZuVPS@r|x6ag;dQEird7BJ9hVQd8rV2De-`nf$T;r04eH3qe^B)G=Whq zB7vufg_B-Qs(2K1YRT1_apl{v30lRbOv<5;U~|9Z!gKS*Mj)|O#ug8`U;fG$Kl>MNfBgsE^QJHS z^~2y2X#3U2*euSN{B#8+KMNS36fVS1&s+ z{7%P5>HNmH9CM4QXZ0-*#@HL3Dx;_W8^#)V#JL$evC z@M704?bg#!GAkLTDL0WKf+=1s4xzNojsZA#ZKH+DE+s)pMx3n%JE0d5pgEcg%NVnd z3#AG2wOlU6w#rtf6=(siZOhw=Fz=QA2d(hGY`HS=VN1O_ug__mr)$SC^LVuDa;hCVB3BF~hHf+rCd|OV(Cl zwoh%M*0i=CHb}E~;)2A^2TYYZH)tYkYNEIDg3`^=vZU)ei&bh4qUD<7uG>0<=OSBc zLRQjVT_qTyWitCBO4}aM?RJ)%y{1-r3d#0SCL-rbB!=v5N0)Y^y2GOjsqP3f zZ6w;+Q5Pb|7=~OXt%gOY05QaueJNo&?sKdonRXHYZjKmG6;Te8*pA9zN>vwTc)}0A zh+rdu%NEk8iC2BpF5Q>DgM^Pni?)`LUK-Krsv6jfrAs$d6;tVFNZO1>gK2e_VrX5# zMkaGQm?Y&>+KmP?H0u*!d?}K_BeXNrKe!;HWv@`xvOwa=v8kbws&GcNNf@w@XalTa z7^-4L4#}>?yj&1n{}vFGBE7C2bl}#Cz7z~Ar{rq+2Cy6|VUCPZv4~D@@!6NT(bp9h z(nM|LWOr)2Zp;;iPFz*kyC5_{LRdOm`)) z`Zcz6@3y|T2R@nn4K;;z+YW~z_`8nC>c0rG&tzi*P#U|f?01$G#p-1rRc3M1xqHft zJ*Q1kFRvJHzfozRI@VSlR4*$H+;>wldtkdE^)kS1+phfOty|U0?kxNK^6FFBTijy4 z>Dmi@g!zt}H~##VD}H{9um8H_rfbi?{>qgv`})Z>m#CMoz3i-8u07|fi@)-dtH1rl zPrc4_*QXr&=vO`eaeY7bF;9DddimwYKK8h0@AbSxAMo5~-S4aVQ;91xtBcLW(4`lhr>ofHLP;-~sE|;M3W{0u z3TlcWc2&QUU+KzM8Uk4sKPs*3PPcBp)wWf&5ds)efLDWK>vp_At)#jbpp+~f2350_ zlm?-+lEO);DRAUPC~9F;CA)fBu@6edv8}{K)BV`Ql%l_NC8%K-GNtCExZu_T?Ayq?@&Wp=joq&aoUyHCAF~aK> zBJx^8<5F~O$Z1jGVK3rjJ2@jKIWoc*uEp0*foyb)bS{|Tt3xT0VJ8$uIiL!|(lxe( z8H-yRrc~(!%p5zE%PG?hk#VUT20lPkO{A1qQaG}k2r&gNd5t4v!UEvfL9+0omtG_# zUke-+WQz&I(kOjqK#WVgoJS;|09ZzjhP)bM2!=|xV-dfWB}@z0wP=}PmnI2QRxVJh#XdmS zD{PbMl4niCU_wyVl~!_K+^zr0A=rL{TXdl%U_Gtm{5xW04OedpTVd~Cx&*kixg@e( zSC#-SrippwIiKzOpB5y-)mi(x)Z6@qlvbroDF6Q3U-X&fxy4K|#8@Gq7U-dCMX~DG z^Vq6p`aGE0$2q}%<|mt1pT|}y-+tqjz9QTG|YH5&1Bw5N`>iZwS9(EK%gSW0`96ct z7k@cH6wx+XZN1v3*{a!acFMOCTkb!dq%H_t+RiyuSI^M93evW1U3DQlgzU zV8nuv6?-`kGg?F17K9zMEe)9pVm5MZlXh%@ZRgtc*;KSeX=`EMV%Nc9R9UKJ7t8AP z-*`!uzdRi*Q}D`n2b{vrxMosPWt8Mfo~y3`Y|Yx#TE;cEv&%SEX>9R3A|lQ@cbJTtT}M_*Ei47zP{+;`bm1MEHbNb;IN8b2T8Ea% zXkO;q3!B|mN|J4uP0)h7t<$dp(bsEuss&&VFx%#p=-cngMN-a?ghhrdV9JCgm~jq)G=^~4WF-L@3ZqsU z4y%Xl%Us=5RVWZPhqdWUT$bc+;$jw}Q-i{{=~H%;cuYz-LgCKXKaONrVvL4_)-n=Q zTHU6s<<7`Am#R5{r3;>fkpihj>(E;YMkJBbS+XP7ZDq_$Zn|P|Uc0HHuT%b;Ikf(_Bk;YP&SKE>T#V_;Xo^(Ckcv zP)b-Vido23ZXHddLI-9fTuNr<8$j0(uhL^ogu~oin$FDVF2MiyKGrP`$`>)YA7xE zfP2@-zHS?`qvp7navvfy!4Xjs4`3^V-9&cpm`;?&zx}oKv8(rVp9h~-p2w!>GFAmB z1gege35sI%vRi3}h_0c2Pf986VeH@j;+Eh1;wQfB>z1vT-@5GzHn@n*v9G@5J2!0p-bH7BZu5`-=8X5A?Dp|1PJFEU%da@$F{hsN z#8Zym_qb;~exf>OOAZ{J)iZY-41#3?ngcS0mnXTZ{L;u_=oRuz}~y4m;dBN zhxsw7mmT{I_3~T)XyyC99`UgI%=dlmwyO^;-`)3p`S`FxSS{&ykkxMQdFNY{%9gpl}U2t(LC8qJ~r2hhUf$BaQN zp{1&;M($oPJ3_OAR2_52lW%o>`Lh7 zkDtI>vF62*ZsKTH^YlgEs&zk}CDjtbFRXa&aUv0$L)0ne=>k6X_iDxcB6lZI+>z>h!o%Olc?~yi#Ea_U9&V5E( z-{tPNYaM%+7f6Ar5vLPWTEbKBI$=zsEUdAl}7A|?2wW|=MS=hHrKw=h0Wdw#F zEW&cug_*dz5)jL%wRKgb`>cw!Wfu-}KPw!YE@sCi;iX%pT*4gtLSS};0PGWv5)63< zMFK3m5F!U46kSGB1IvY74y_V$M(LR~3$`4asUclPA_3`DZ!!W~F)N@=N&pg7@3UwP zX`6CXV7i#s72tJ6I9_~7N#*`c+t0VJ7kOHn?+ zFm%i~MX_BZm|1Lx@=6C|CsJZ(WWfn9I!lgC873H77)H7Ag<Yo0F0n{}!*HL_CD}HzWdt)j zE-+||Ldk`aI~P(u+5n|0B~rrjTvq^Mpzy)~)>=^tjnfdL6Rml%pk*{WpJ6w_To^Iv znxL(|FIDT<8U4Th^Y>n6jBXJ+a2U)8gXlujc@?odg=Wb(o7koUAP}AOYSEqR08Zph zO12|e_)@&8;d$xG4JNi7TWKJK3|ye9Dm=}a7eyCmmcWxIBOMGtEG@l*m7VVzJ4=ht z7amNP%fL!$IHx8M9D)1HTt1Oc@%Ld0==)v*Vi*6#z#V5!u~OH)V&WV^nCE;V~n|nO6XT zu$ed>p@HcZT-VC5bjmRLFGLs&y4-=wlY&I(*0xmTHsZveYcdkT5qR>6fg>I|F=Sq1 z(SfvUzE#!y zZ~t`j&wskb=a<#XTdz7#y?pykm;LI_EyN?)eu8=H73cek?CUrF@b;}2xxf7Vub+AI zmT&#_CtmCOvDM2yzkK3h5B2$F-}m+WLm%P(vM>8G^!&pg?%Tk8-`Al}-t*aic%SDT z^3bR3x7+@EZ#d}DyT9~#PkiI6j(FWm4<+yX)8meL)?*&M_Z~j3?2hui`uwurmfHOu z8+OrIm6h+x-gUPXS^3SWx4-4j-u;d@zxQ2lecPMgpmtL)|HZrCRspLzBWgD#o?=-` zSMjL?)o!Y}iaQ0YiqEJag)@xOPnWJRq*q;PtCW?fjH*RtuRx?~x)M_D3BVnvpaKE2 zC>uqjNQoU7A-z~EDoWv$?cy4$hNUQh>55{U7-S2~kp&4a0_G`erM;BE+^SY1Q^2^S zz?45W#kg1$qvp#|Vtg^D8-*|VGEsdCEhrbXG4ry9ybOuhim_M_Iy*5Vg*Gpo>`LHj zWQympwMyeY%k0~|`V=#O3R+h|?Z>2iKlU5`_$9vb>!Tlh+vh%Y+E>4P=9wRSqpI2G zm#?_wEcNoWoBd;Flal%7YqvN7J5!t;PCD0z+4eh$SO8QUilAyM#_XM{09k5*F=z)v zzk8{~D6Y`aY74z)H1gqxffOtE>a0c!nmbxUBTX>aB+qD?=BPDP^pFizcF`N9@GZ}x zHzZ0)>RL3S3#OX@6i6(iU6U(yV=$`NL@p38m?{{cdU?8hn~_n~y3t73F&J7CQ5r2* zw83E5358erw4`9=z>ZnVVu-*)GLkZs6@`6CJ{CMeC-h!19I29;r5p|-!&m{3P)nT3$XngLNE*A z4RHdE#oHngj))=wm@K8y5WF;u_O9CdI}0#a@3t_F;wfhqRE#XD4`50aVpJ?JI}^JR z06VTtmt1(oWf!ZObrB+=NQ~Z!(Y@&xZdR|Zw|)tbCItz=Nch5_n$hNxWzv?ph`RO? z@N_V_O>~nj7DpQ}#TUto1&nWj1hq%fl}0%;x@{w!@YE8nawt^Azf7KN=Y_UT<&eQ zmz)5_8Y5;hX_zV9s3Zw|i;x{>c7W`<6stiIE2+q>?*k60q70!|W_50ss#FI1& z36NHdVn~Q)L1|ei|9uf9uO&%F($}~DT*Pl`2c08EnzL57azXd8?F>MVD@x0Qr`uA?HyMgUN)Huwx-Nr>&vz z%?MRI>?Fd5*o{i8E-!j)+IRqZL9&1qoh~kbZZM9J1c@QbZa>!BH^t_p%>$(p((Z!A zJG3fV-*MAb zN@gEnCU@R^?RVmH9(Me*A9B(WkMQB;7aq0G zbDz1_(FgD4Bg{uVZSN=V{P`;nf77dveA}Nq_w=`%eCUCXeAI(>yVowe z+-tYpre5CT9((SrUhW3*yWajL&s3|F)yr@A(^Hkt?CM0do+7b|SnWnv&8ZMcgfy%Ur8tgDzzszL&{ILXcfg% zZvunCpdxsxohy4;ba@G9NGl)i(Q=d+0HFw~nl3{y4Asl-5$ggseVoE!Lqg|Op2}fe zB0~`o5@mAy)z@4SFbu-6i=qV9qD6&}E)8=8wk(#;O@IG?u=17&*iw+yJJcWk*hk*? z$&a1x>1&T$fAj0UXUt>aAEhXe-D@_aRVjNU+mP@3^7-Z0pL)vM{_KxFamG7+<(GQ- z!>7IB>wo|8bARw<_3~94&rvdOy~@vpZ@OX2=58{!$#*HJ#+g&n$>M}^0qKOI&k4(t z7ck?Rp(z=5R`LK0CI%#=x~wxgi__opBol7D8jZ(j)k>p=vTGY^YLcN0GO*|b%xHCA zc5UFMPzA>_Ui2z2bCev9 z-Cu?&XAw(D7n@iPC}#GZiPbijE|Sg2E2M8ljsy>-lnPhdXe&<_r74u8mSet6uzZA| zuy8Cf`pnV1P^~LNyGB7sdTo?LX^g%`@kZ&j<;~9i#MXjPyl;Bf>rK$x8B0Q!Ee8BW z5nXx`ZaqsTOo4ijZvTS>gZD&6mfm5tSr|zeM!Lk>5>O^dfLwSXA(UWa0Wgtn$*ttP zT5+^-qGPZ{sm|iUf&^?`XTDDqo?OR$LoUM=6ns5;UrkoK{ za%^$RiV=yGu{mtCR2yBo#}G;OvSpDfX^9Fd8JS~7tA#NtRb4x)_F61WJV04tW@I7e zg+wOYi%SQaaM|Nwbfe8giiIa%Jf##&cidUll^C=fh0{7Z-HqbTrMP%nH(8l4X<2lM z(TU3oas;PJ=fd-TboN=_{?^yO`n_*|!~Ny|{qMi!&ILIn7ce{&7Oh3gzGfm#XW4RW z8R2u_k8H+W?)9rGxBssJkiEf11wb`RZ z)zU>v2ZqT$DJ>OInn>WP4_-sNM)6p}xo3aR70jhfw*LLX5lQ(3Zh~kNU84mp;VikV zX$>I(E)$Aw#z>kzdsK}Iv}QeZ0h|}EMMg2Sw&8WjWJnppF&5OZU~x5YCG^@ZiWkY? zqe5GHOM6rSI8uxfU_Ni62@Ao! zKvL~>ScW)BL%<|UDVZQ?QY8QhGizlhCaBxK-Q#xqyHdSPNc)g5mC2>OV`~%)es(V` z-7z;u6EQDa8W`=6!>@?t(&P4j#fgtQ z`IvoAe9j|JIP4MX<)`ky`?H>K-&ddXl=r;hM0b{z%x^jM=!2i|5cTqXcinKG-T&+U z^6q=w^KR;8b(wluU8YP{HJ|#bm#UgQ)vS)|-ma=mDW@1z^wB9=UMf2+>4{<(q9pWl zPLOo&N@JF4MulXBXHO|}Cq@;gia|Vz$8M@>Ar+*C064nes)ScpqN?;}q*T>Hszd2& zUU*%KQDv+;6iEOsnCXOKBH_gn9bUp*Fqk{(VQltbT3+U*Zp~tvr20BSR5CHD^-I~jRh%QMJ|{ z_KEL)%Nl;n?Pjez&MIYvGrJEj|K-O%^yb&S((g&Bm;d@RANa?we8ThCKELe#@}`Tv zbLC|}@)OMJ<(sa({HANJ@(6Bs535t!`8#)Xokvcbt`CX=UFVK7YhCFq8!&vZb&DNF z%yU+Do_3DAwJNkB7`ds`v@FB3ZgfWJi5_Rk&{>Efl4JU`<3bUlKt}m$Sz-t_>KVdOr6WXPzCRxG2s039Zyj71!mp>2u& z`z?kwV&X(Zt)b>c$=F+GZ&kgK(R=?|4tbxwOSXSlxOX44=5C3~PTU^K@ zM$!$(GKq?Af~F$}S(#30)D8w47@_3uGpCB3AD{cfi!V6WzixCfxJ$@IL@luBjD?n} z?vT(~Aa|UXA;Jrw^xB*=qD|1m2mD%xImTzs>yR_`2Xks{@-8u@)ytf!C5VoG74;dMw+)cfy{*NjmKPpo_yF%wJOdX9bAoY(X& zo*0TnX;PB|1^F0s=||C(u-V64=+}jhVbT3QX_# zq<4I|jjs>|Wu!~Bh=NjPi3HMJ4i%b94BTav2wE~q=01E~K^$5wUREoPrhRxFdg@5@h0^(p4+=x#R~_4~`WZr!}?s*8xvFZ&ep zFK_cJ%-6WNeA|s1|LNAv|N8T5fAh=h|M`xq++4o)iXU8a*^h49y6MKv=WROgYgb(G z)l1L$!mn<<;LP`(a>P^byHXLK@R*a2-sk1dJHY3cr(X6;QvI0Jt4?}?lKF*4?dy5$ z=RND;hd$}Phaa@pD^EO7nf&HII?7YoZ~fzA4tvTz`#+|df;SG@E^3S`ByPci@5>;AanRDo8ls!UV65v3x%8c?h5Q{pKeE2!tD zvuaU=2%ysRY_keiEve$GYF0>6V0f{soju;HFsxn%VrchZe1Q&UN(b;8NG78(~qRTN(LHq9kW_Vz^(T&2>OB=ax zz-U=a%8J3Dk_ksiGPZQ?Bdd;CS_fTVnGloZjzoq8XJz{bPkVh#Av-EC=rY2ZCWKF-T)1i+5z33U6M$d-mSvt&Or2qu$hr+*(GdmS_3L zR>TgUQNo3<_dzoki?jpj9dr4<$xbSW*tz6eTwPfOkT5pgm3$Vp=KyR%AYldu$-+)S z&aT_LbLp+&nQjkYd+&~+#I*6qmY{-W*tV60E}nWO&(F#2ym5Wc#M z^4D^=p_N1Vv}la64PnS)t!cTpB?ca5mV5)#F}EA6ZC>=*Bo`EHWCB?Vhe-|vY6Wmo zU{KM`3jiAsDNC43sh1(qNe*@i*Tv`6A2+KsU8wNsrEg z2fjRuuIlO70ptr4KTEpSPyjyC69uPNS$C-wv@ZBPs%be@uB~+h}beBnIpisb3_cv*$b9Sy~YM!^omb8fsFI z2s`KL*yb!+i7}3patCHH>V=C1fG{>AE8{2w#8zmdg$TwNxt2ci492%fNe^d^*f6(% z0M1+2y96+Fr0{g$j20#L*ub)bjVR{cnrP*$8$%d)lH=pPv)j>jBepAT%xv&HFK}B| zvAX-W*K)^qyB|U0FN7%gS;QJr8bxF@J5z&aPo^oAl_j#3vfZ)~605C?xb2|Yhe%bP zwrv&E5?)XMAltE^qJU+@qpOWa8xA7OuNAA1zelL6WqL*%Z)S_+Pc=T@XIDQ{bnLqKt6A#^Ir=7O^v%bIVi^~3j zdGHk#^HMyj6BU&zMBqwSMizA_3%nwfuy7Gkg-*w&6#|S=*~kl+ zP#6s{>e9uPC%w36QDw(gs+4M2*#a=;#Zhzxd$BlDuKK|xmGI_;2cAgYBVhRQ*X2m( zLa#cmyu~mq7R_9^n}r2Y9k)JaSJwhcTObjwKb^D!W`qYDUqUg9bIy9dC&(&gqEc2J z_b<$<&3b0j6A37DMVCSz633{Qk~zL1@|y= zGg*OH#`?@ryo!nokDqTYmYs_zd>IR^Wi$lCK5}N1Bv_Ic45Voqh3q&|U|6)$2rv#b z7XZxvDTxiCxP^Xl_sS(3X}2&pS@$frF!wTg_bfNWaZfZVNvV$9iZa?_wG=QjGE0jJ zt%|G-Kv7tTdutooZlLa=6!vbfr3BwCl^sCgP{I-$4DAHE5d-KZV_v$vVvq@pKt^L2 zckMX}7yywup_q%TA}{liGq$ECP)U|;;TUx}A{n!rPH-dxV`?x(X$s;J#@y{C_#z~N zYVi{;x`sm9eMQA9k+a;C#tywr=`D+|Uv|@teg(+qdzH^H!j)djURLSs4ar0THh5(v z1fd5*A`M;9?EobcEk$%gNazH9@HYiRs2ZZcv!KdIPk7Z271CWJF zA*5xBnX{X z7-FVyZ#B$?7yAg!(NO245bVG)gOx)TJWYfWC)m`e3+d30(UR9jxL|9Y^BRug7E#+2 zYBM4)&>W-86FHam0B}r30!WNHNeE-7D+_%(A;+2-pkr#4{*^CVFEse-^V~(&P#xY7s_?8UXLc3rk zP#{B;sNjpD3#n_eys}45=Tc%Ud^_kd@M1}f4j0wpC>U);RFv-Ko)p-Xx%F2s+a1|p zxVhU26tTp_lC9OSIAbER8qySMLzaxhKIAxly+adg5jJK_usp-50ur*s%DRm{jOJk9X0*M?4CP&#H?yoEc-mRFE9V(fjiX8 zzQ26$?KiDndey~WKmVeyop;gKKK0#ioOjtpe}C=eU%BbpZ>pDldHIpMm;3(mZP$Ot z_m@{L`T8#&y7BDOUfusmjpxhiWj~JX|CjyJS6^YCeZ=hlmv=nw;1?b8is$_0)=%-1 zUkC2~+&3M&yANZZ_KqV?dCMWh?|tpO)g~KnutEPI^&i&DTReBu=RfD!yS`*6kC;`p zo-h0PFMrDH)53~D<)QLTjaeP1xKqTb5EXQWRC6jpb)S~Knh}y?kAq=Uc}5vkd{vQ< z0EC5$g0wQTE)0G(CcB5W+{06J8x@_dc*Lw8SH>z`b-5@!jZ#>AOxma%h-%a)!y&Pi zAsvaJ+Uu^q9G+LzwBP>nSiB6uU@DyPFq*H~0XL*;RL*GSP_|qkWnzdhnh`JjD(Ea+ zEI2O;5@~q8993QzL@6Ri7(70VtsGV<_t#MrkC>IrK7H-cvr^i3nE^a*?lH74FRPb* zdD;I-opRENXMW(FU%TM%zWT+pFaG9v*Isd9-(Q~A%dUMT1{w7i-L~JWK^K%OMLAFb z;-y2^D6kh0LeXV3M5-6G^j=S9N`{4GT=L^e3^onqa_$oL-3^y8VabtK_-U-tDBA%G zS$f(qqEuLwe6J65c0x%vI|7MwFvPFRZUsCP?%}c`k&GNL$Pmm51CXl7(#{>ZQ5N%p zhf#Wi6^o@E&4FbnZfH zaW*=@xsN0<Yg*}lRp zPn*(R%&oAyty>t2+{O}(=B70n1V*&DYR;iBkw$P00gDMvI|53nwC@d%b-N z7hPbOa^POMU0kCq4aHw8nXxj~9APj<-Ht*4IgBO6qbm&xysWU5q;4>yBs&Hedl*9w zjVOi~S?r~C=^>F&im091$54xar;QUliwT-9d^@Gm!(2Fa7CM0!oeK*u>l2Kfn6kFe zz!EO)?2sgw=m3zEGJ&DwQq2xf+FV8gU`TRIm(O&Ru{>cQ4J|0Zq;$5+>6wI#1EQ17smbDVVHEV8(CzAaSzB_Lvp zhBjGWK9;nITpSc#_(4+g(^eE+n7q)IKo&!~xtD-8BS$iZ>T)-QslX4Hs77nn#Hck9 zt~46OhHA`_F`hv+!cB9Sj+%QUeDMh67!^gJ`D~0+J7hA}vt+amr4yqB=C|S^7qlVD zly<}&iYUG+%PU9Wr0RMvA1#K#COW_fG04ZfhSz_nNN>1Hra;r2oBps<)X1);70YgR zqWYS!Ov-JvBWSa$JE9R$zsoN2Obo z+L&`xR1&vu8z*;j`%l{syRvlUq#=6@tv$@l#%cDnsl_IQ$&#W=z?%1RL}!H2iq*o( zgwV~RoDD@HRkZyOx0KqXP4~m}sg!wD^ZeEpdirthwnJvoe#RW_VT349doV-uMO~~? z?pw?~Vy$Y{vuJZlmyH{L(>-ArK~zclFW6?&r}zuz{dfI~cO3u9b54ET z87Ccc!jUiAf7i`lw%xOLc>czlZ~T;JJazVmuzeVN<{zcj`+WJ&|7;__lC5Okf8V`S zWd2^tr?P#2S;?&2QvfRHJQb}f?6I#Wq{_D{O{FelrJU-|kdjs*sj~K=ZiT0kmH>O6 zYY3;JFGUGV)P?{GL)EBOIfU36K0Sdt%PT_mtW3RH*Wm3Gqd2S(SjyML}gjxWMDr%1u{R;<5k}Jn+B( ziBWb&Leb^Ot5Vv>+Req9bHru^*tu(kGwOW|)ik@h*w0_9i$8nbN7coCBHM#z&yf|# z9x)qY_c3fmw2x;q`h9Hmvfsyk>xsv{`|Yng{nWR9`EzG}=9BNg^y1GgzV@3dZn?bQ z$M)rAzXt!HZzDcYSx~AqTr94duAoW`b%Elj`iR9ViO0Unmn|U_UB<)(Na!v!*K#}y z);jm;`unH&e+Q)kO#|fh10L zc%loB!KiN2;hS{4YGDW+k{2XLEuCZYijc9mMKL7X0`Zj(dkm1e!>iGz#%O396viwP z6PIqX&W)Jk5lDK2(-3J9L|jntEGR;k=QIKQ-3S;1P^7`wec9fr-Mc%NyOo<-l~>*F zvu@mAsQ8+%gw4omNY!*GWEvf8208m3G`B)GM7n*29Xl$_B+jCXNam#Bj@doEtYiyd zi=xV)qvczj#U2K2RO|$xSd5}$(`6JIz8gJ-WkG?5;eubpZtjYm>K%@TBqJq?oj_aA zVzC!pEbfdfa^UN$&>rUc*=-DH=Zm;};Y|Va$P==zeFcnBv_>s&$p8cf%XcIr4K83U zI|WkP#Oby)yf83A1}*4=MS0!k0`uWvJB^U4{;&(3BG3 z0&l4F*yU{TWeAzOAwstlj7BAF9az|#BgaG_%o)*A3S=(_pUx<#(0B@gZ4l<(vdnM& z5S#^R9ZIZZSf=4h1C^sMF+-gs&K#MDrC1BdLbjwhevC9xL21dy^UY6HIDgg>3@`flbF$`Yl{8GJFLWfKa z0oYpsN<5T|*^v~eH8C~(!jWDs3f+Rxk;4SeSI!YK(#1tcA!8AooV>uefVB=<-4?TU zq{O!h)UCXonA^R3x=#;MsyE%!S!^D96e~j{81N^}RERQ>OrpyOU#d+lqY3f_bGTUy zS%ju6Xq!X3qxKsTXg_23LP~hF{Vdr0)=%ap2zj-QF%g@nHg#G%8L`@WQ5F>CuOrTm$clKHWR*Zk~}+aG&) zm9H@W@Xp0|uDW{lvddQ7e2EWZd&InY$z^M9{=WZ{T6W!69$b6b>F+)4;Jr2-zmNS_ z2W_KX_T$)pedLQ7-*}9#E$`s}q}0oAJZ6`_e)UUUci0XG@44M>FWUIUTRr7fFMrOP zU$dvrV;{9>+udHY(H75oivN>(;pQ7}_RNhpe5!9R&(z9KU;qC}^{-OuKW4WoOkt>i@xT2#{ zeJhz!>GG0>7C<>H^J}iW6i=|C2#rC)N@>}e4!uk;v=nB|i!QEpkcO$TNJf&_07NlL zml9^92vNby&2&6;rsI@`=^G-JB?XTmT}XJd@YL9MlYQ3O=dM-CS|zg@`D5pNP^+tU zRxiU)0Q^4oP9vMaj<6quyTSz=YrxIR;&E<)+nz57TsTveUtgCfv!e)2DVw z400f%6bla{DSuHa66MLFTk>HzmaU=U8ifbL7L|y@kyl;d(st}Tl17565*V1o6BWHA zx$|;FB8O*bO%Mv&nIP5dknq4->!u};gt4X2mTfPs0NDZ9=`7PB%!bNeXrkK$;R!%f zpkyg@7%eG#dAzB*``#{njAbuf6&~Con!QFYg1-s8qG3_YwEmf9`U@`c)7*8lv zF_mdrK@C-f(QV08cH9|xQIe=?+fst{L1njcNJDK8Oe&XxHb*(@;(d0C z5~g4Xc_GHEbvZV*k&-TvPXYkOC3~@Q=@gZJz9l9KFkeV|r)BesA)~C~9C5i=b1OD7 z&r1vJz`>hc$DLR6l3U5NCRzoKprTul-jgW=Jg_M6s9zU+(tU`3_pq zV>TK<@&ruRQ0d9PDYPcTt5OS+i*;5Lbq2=C*yg{WElB%(7NhVO3K0o+Ql2G{m$-5s zDNWM@Xx6PTScKDgahwYGgd_Jcoq=g|gloA9WJu10GL}*0GF{t2CSRjoCMpB--PLhrkujayN)Wp3aDe4u=(i55m!|)vuF-HzSN``{R*$&@y zU30JVDOifRu=|a%TjkB3)iIVF08j(dAlZ?DED`J z3^#OQH`0E_T^@2qW-n*01cpu#onW3OR>W3bwuVg2QaIUwv$RSao1ipAxAU<*lT2|l zrc2ML#1z7{ie;&qg?$cV6?bXRZ+q_C)DR|Mf?{Z|rJH?bz7O7|F7_yS{7|-TM(rPe z{Zk*tRw=8O{X~+lHs85o$-3o>?^$!RlKB^p-c264cg5YS7tj9r^7Z$vz43v&ZhGjR zn;*RUCROwO>u&tv9k<-E?3%~!U%7VK<%@st<(@D9&0oL5?_>MlD1W|u$X=Tsvd5-J z?>qBnQslUUU+DSr?4L<(b;Q2UBYy8oz5LpPckq*6FW+vX?Vt0M7jN~{eP6Qa!Fz6T z;O@`cam#1;y)Vy~`#g5P_4Oprm$%zyn`dme;ik{})7^J{v8vFAv3-&G zlPcL%f^=mdqw24U(vYfFSqEU$1Ldk+6{xCE4d~IWl3A5%RIR6Yq*Od2LaR*KWk?JmQS-_Wp_YyTKo_&ZmaaAAE8CK+uvOKj zz{}9+wO3sRsa)lSK?0JIiMXm)0O2H$)}$2RDseDUvCPp@fJsS34AW#ItS-hW+t#G$ zTv)VC2e?HRx1zYuTKjb`pUT$y3^t3$%{^%D=`*9zZ+-O(7k~YNcfIBKH^1SSPn>;< z-~0Of`KJ@V`gP3@zN%gxzxU-U%y+F?K58@k91 ziZrZ@A_$M%$|MGC1>yxcM0W&&6j3ZB?Tf(hz_8dLwfz8N^xV@rOHo-9PrNNG89HS4D5Va0?VzSQE82qbJ?=YmytloL84nSqL>Kg zr0DCb7smQ?CnGTo93vMIXW=@Br)lTae48CdFc!+79n3h%G#|*)fNv?LnHVL-1*5ce zkDm*PlTu|G2h`>hNU!89h?Ik zo_&Z6O@Upk4#yzJlQCMk@qz@P=n}eFI~$#xE;MNahlDAUTnf!%l%Ar+AO|g-BBJZP zPitg%R+Mcnt%)c^=8V%brzqvw)L=?DcV4Z3`O{_If(t~!4;i?Vyl!#n*hKL{p{RG4 z;UYwt8)|f#aPwuSv`R=YAY{}^i3JZ%07Jy2tS1*#FePTG1&SQTD2&@jt1&V(B&F(5 z%!SYv7XwG4O9tBv;S=AtveO`L=oWPLB_dHLOy(WT38uBrV__d`}dfGBE z7gBemkp<6KlNrKCepoTvc12L3OH4Al?Pp4xeH702Cx-ZX4nDFi=V=@HuyD^McOix^ zMOURf#0RrIZdN9%k?E8_{PM>tWtBxO`@L(H-o5JPdvCwRSC~Cw_I&w)JD06pe%-p2 z*Urk-{!+@r<-1m2fyv=kC7!Gj`c>!##G` z_{e>?dBYJeJ@Ker_j%b?d+xl&cF%v-CL3+^_-}pnjCno3kGwD`0gY`=M;5D;P}Wp}Jd5TH&cq1XKF)B4Je7 zDn#WfOVulP^{HxzKGrVbF$F`q zxl=?jHOY_`+4@-0(v$ftn|A{|*7xsMpZ8?hqh$56r^yt>GNW%aU+_f?Gyf_@`NjpG zyXb3Q_Hg-aZ+y*pAOC>=lk(+d;{TWZpVYF&m-zDXovWAlacrN*zSr;DJn`XS*MdvZ zp1*B>m*@OeEx_2d(*^5_R`0NMc%M+2~)y!ql2g1uvNff z7|A!JHQEbZ7P`1v-BJy}QCGP50mPC9qi~K+UhHN~Vt^^h0-<8D&e|-|BB8|DjEaI8 zjD;giv*t@D?CVrHpCLQ49&4m|FA2BNIsc zR@Huk#b}n2LA-(j%U^Zm3R;I^BBz%?B#?o{l>nhc%Z1Rnj5LhuT2EdLIgYy6#4236(Ws_b6%-5!fFZsZ z*jchSBs6z9m$sphCCtTsI~Tr&+TwQB+0TNXCDAoRyk_f1jS8s*(GBg2!$BZa&Pq-1yjBD2w23K?ZoV}8=DRLE4tCL zr8C-c%hnX^Oxo&Dbd9#-n%{CC0%@ZqTw9kWK3szNp0bZ#_gBjm%gW?Ljcmwc<<-k> zP+a&hw%_~mXHqj?VP3oJp}SZ7=>9dHGW)%++i$(*zO_s4UUS3R6<4jf^#}e&>W<}C zsG60`{(pJ-jhEfG>V`XSyTa$O?^^M_m5aans=YS#BiOHg)mHvY$|L3@_I}Q>`_KG1 zwy!WNlYJ!n#G`lek?hwVvh7j(ZE@HupXdLi)XRtL^*sN-yz90bE_&%^#~=2W?>b>W ze=K#_zB|8aj~)JU+s!xKc;@@dGqv)Q*2~8nc}QPj-et#a)n$rAe=Oxw*?y~89jacd zG_FWg-@&LW6>jV*I2M(iQC@u{TU7|7GNqjK&bJ_}qEr>~QfVqARfTFpMJ4xYUZcM3 z?D4K4)nypW>RH_iTRL|x!f|J>_EhG3GLAvD%OWcoqV>t=N?NVYb>pn;MGFZaNn)n( z;(AC8BbFf{%$<%9SVhkZOiP-+h;*%jS_m#O)Wyjw5(x{ca@Um@5s1zRW0XtwlCfOY zO(ure70ZN?-m)u%6~*dhbupp%s&XH|Rvmkw-0y&~)b;z=-~7tw{W$hXZ+-pQXPk7- zhu;0AzyGj@%Sz_&U-b8julcs;%Xh4}LA~tse(3I*hlwsUt!v69rXC>~*@+j5UMm<$ zmm#}KZ4_Buk1i+*ab4#N@A+~iPxjc5OkaQCqh*}xQ%updUB&PcP>EB-lC#$eUF9S% zm?;l{mzQJ&DnmxIK$Q!A?hR3*Dx!8uJH_)tDTyQOkRry(MYozAi6bJiN!PdIrOhJX2P%=`I>{@n#1`oZs7!rWNV5nG;7>YJ8t@M%zM?1A+82A?1 z<{zLy+Pq*yY4u7#KGk{{z>KoQHUg!L5Fv9%IA?TBaIDGt<~0%?3g!_dBetd>=a|`H z1a11J*-RM$#Da$b$hS$AL!(+l#flTyA~@fiF!1psDRh~H&xpbFhg~L}Iqa?=LxH;r zguoA+aq#4>mA}>m`8rpdDHxt19WtXZ&B##9qH`BoloAMo)>$nvcmSg~n^6{EGDb^M zca8+pdR1>~EgCk8ISvw0_`+z*O2SqZ0FoV83#L1w7&SXc36x~C?l_88kH6hn#$68s zaMB5&9geMS&s%K_+OD)o&~<-L+V*JMfsFi{5ppkmCM#CqnvtwzK9XmI9Qo*$He)bD zoea{jsj!id$;g3X%ONY-mgme)W_Ev=oeNC66gpq+f`WN!N2p}>Av$c$-6+g}{pM#6 zE1A{HK8CGc?z7pI&#Goa>sH>pYU%Zgi~HBD@Of-sUY;qL*DmvyQXVm@n(tY&#Pj8c z@4fZGyO-X-Zt*?0U+>{^qF%mh#SJT${J{T7`Tp_`Z@;>F`PB!%@Q_z*<{zWf%SZ3` z{9_hv;s2NYIQH=eZ?9NZDW7oEj@rLE^hL)W_=2PN-}=Z!Tlzfq(fhs7|1a;k5Dv}TP^DP1)US=kF9x&^Ta@TSnD z=q;dShk-Asbt8NkJvCM)f9BJEsN6!SsXwNC_I#NrrFH$xmo~+Rx6eH7jjQ73m{Bz&u&zJp5_9fr>>@}DF!wpwmwDiWy)~>k8w+ws_xf-M^%Qd2^ za>cm@s|)Cch^wy`3PZZ8mK!slXiU}Tcx zh+YVB*~@_?Jd3!owa)aqFqV{Vp~}sSc;Oinp(ik^Go#HHzJgj;hU|DC6~%o>z9-b) z#Nb$5qq$H9PszkdZ~84=nC7T$eYAm%ipxcp-so^|Muu2Aa4jQ!tYinbr)me+03ZQ3bk3P>;znOH*>xn-B<&5N&H zwfq(sLothz;7`VW9w!VVOEsg=Bl`4~iUEveWW`dpe3Rj5Xi8hfWamI3_7cifqEarL z4F)k(0fjC5O*dTY?r$?75Jf9Gq_%|N8Jis=3`di)0g$cHXbrJz*@4T4Bau}FJDq)O zZ^E}+xr;cuEJU}N)^f2yl7qS1cmR|nqrH=VjPmJ0v4jZUHbx6y2=)b~%^1#BRjQcH zPDDAFZ+juWEM}pFDZ`R1bI1i{PSC*G=^Uqyq6=Y^qm&2*-VP9qoiL86ISR7P2mm~$ zfQrty910Cj3CZ5XA)N`jsLYWU9Y!E)-fGASEKZ~4R$Es}NMyo&fFl$%Hla%|Dy>7t zf`=oRa1vmbSW>D+laWA&xT6yWISeXBnJ}_cH;pZKX-K$503$7?p{AgvBjKyE;2RZ} zW*V{t=psfq1x$_fTuN-ZlOOp5B*5VfL5}WVV2`KdW1G_&8q6`skg;0H5Tn*fWsZDX zfe|-kc5z1z>=uMfSCx@q5W@Pmu*gdwCB226GtD{knyxU~G8As`M8&H$AGvinjFwM$ zx>+EhEn@(=;Ps(-ES2xuA?=L6DQt9qZ%h6V;+EN9CdI zL1G`$2Btf{?b+CdjZ6fUp|+IhB`kq*E@F7emzZwJG)Kl@6g-UWG}^0(F0?I&eCQ=f zA#(0}$~JQLMF3#H^maZh3kt8MJRQZ%u^k${1u2}0*f*DbBwMBIGuSF+&y@Z4*KdFR zh@pS_)lYmF+mmI+ei=-$JR|F7ej;13tZKf0?JXWLtCweG=|lJ2;%Bk%yx=IclHhsh1VYuRCbF zK8fwezK%Iy%VQ4MiunFAIsUMnk2`4lLtn9_zhL&wWj~I6(uw=~KPmseeBhotzI40i zZuU%{z@B-wyus6-`t%JqT3@}aDBE}M-8b3z886vodzGdFP!+6RQ=L(iqLpdvDn!Mj z*5gt2quOl%kW|otDgAmRsu-mBoHiE~sM1e`sIK%h7T9RztsyQdP!%g$U8Syal2JG= z$i+e>6Rq6DRf;Ng!Ac2+yRwuW7zycf)OaK;mO{8ky?6xTQVzmNTdB^%4otA=GU7*7 zEX;=3rNIJR{f>d1E;_JQVuA4tU%FANfx)Q6tO*Hhrqa-5_nrd`up##9WuvpvwVyrr zWB#exs?Pk?vQK4eFaEYl-yc$AnE7lrQ8J(R$&Y;QQ-Al>&!6YVvETIiqt89()X#qM zj4yoV4BubA;`?8^@tSY={<31(BWCXcRWG;CcV)RcTq9lwT%oQVWfz@vg?3c}7&R0I z%vDKe#{&sZY1DPf;wc-9B4RxGWu(KfqvcB|Q4Ix*449yV1-MICbUKU6*^j~W8Inz8$#2RWEpZHbnaZ5MG@z;V=paq$;;E!ngaJobWKNy;>xfj*(F1Y zIKRa}3R))NS(P*_`rhvgZXvcz!!rts0bV^_?PJ}GB zm%8TI;bX+ij)Z$rYApz`&`nmil(N&M(d;PcID=s@q>DD8b1_=-CJtZ-xhV@|D6baW zC}b`zNXsjEu!iz&al#;(!luZP7$aJ+a@I8@2h7%i2rY*J!%Vmk#vxfUmNPvt!W6Xg z52(=PDPk^8&e#k!n!6S;Roq=QA!~uH zz?g5jF-|e`k@ouLRUnR#rB^+eWw04-c3MNpARkH$3jC%cy}@HHNpsiMMd+M#nRvA! zsW>1qp+aTH0x!V*-0*(=(lsrYwtw6aIi%uj@C>cf9te4f z6G^fMQ1KtFZ_V^A+Pw0 zzQ64GvSOJWwaBNkw>Wyg7x{^7#qtNe?Uj4&_N-_AiRa4-N#&tpO_8hmQQ6yulqmG*O1$v8 zN<2lM4?HV}Rgua)QhloH1K0{!0wY`xVYTW?7#1PI;~bA-J;<({G$fn^3<0QKt9C`# z#i`oWN}C0zN4-Q5EiqjQaMU#y1!-(_aYgsFWqPG95`-#TJajD#Dm6GAn^B5nILeu$ zOz1+G9cFmz;~6)?>SZvFM$t-!T~|I7j^3sKNUUMYTQ%+BvVV#C=toZT>^S|q-|_7G zlb2oM-?Cl#4}6st8mE7XP7t4G%+ z-DtI8S05x7;AjCt!gQJIVkQ)$8MSzXCX|fjiH8MeFK903I9Yl%Km{qT?dT%J z(*)U3Wl5JnNsexgT)dDfmKC0S8HFy-G8B~2QPUmE0>elc1!{?Ok+USa4KX%NmX3u? z3|k-$&mvW#0N#U08G%Wcbzm`rbZ z-QC>ayzOA(Ca5)Eg!Hmy(Y1PtW#^T9&`i+vRAJpT+Xl49E@HP`?bIj}380eR*p28i zjKgSnTbTC|SZIu@5waTp(;Q7$uMuTt*F*8-NUf zO%O&`xRMcw3nK-s=$Oqymr%H%g5g3SQLz(>E+f4_I1@uG?Difk{REAb(dB{=0H;rY zd%i4CD`~VOl-q zES;`cFr_UVFke%%;}Rub0Lh1mkQWl}j!tX{OIx=<*2*Yj)5L}W%$736kXY%G;l(nN zVHZ>h!;!*ER*YO&X7;7CDZyunzx+#5EHctFI~Y<3%;pH+WaEcvBCR~6_2dFW5vXul z0LidR!%%jBAuLJrVu#N{k5*SWRPnF_kI=b3 zp~O(;lu{Cci+0S&kfqZWT??rlX=DdLJ{GXmK-FBr&;f*Yy>tthH~yWo8D5{N>I)$HgjEj4$_0EpWf!Vt_e0onqxY}%4P{?d?%SQJV}E?>K1BXZt+%YY%?mv9H?7mzVvYl;_KSAKQ~<|GcbTCW>Wp$Sa<6 z;2xetzra_R)ys$N?YFm#SX%l}E8 z_lZ+|dHKRGo#QWvEg~OR$+e-(d|F`lV zc{49xld2ycCreTYMv9`toh`ruHKdJ7R@O6SM#&X$8>`mH@TTkQ5XNbtCu z3yQp5fqSJ*LR*4v>%|@SQ(bl`4GG3Q={5}}in(yKAE4W7Rq%4Ouj{!XT|^r^K^g5~ zU`V?@Mx5im&&YyJ+Ui`726WpfV&kP-R$SO6CIO+@k+QC>UXZ z84E(uWi+p`6lItrT~I?_3%Jus^W`OHU&LN@+fpQEAvcRpf#)7iODBf~1)I>2CPiSl z+t1KjD;5keWhlhZDY_U0r7S4p3@p7!BE&3&l`#=*5nCem5fn^%t%I{mIFq#kCMZvK zU@|9R?t_5>9LBSFAp&E@CT&C2MA#skh$xoVX+U8ICaAcGYhGIUbT*n7i!KG2ixA;; z%}%>up5|U+Fv#2I^DRnO6CM@}07QBgb1W5y8Sx{MS&*nlIU1i%B(OevBn6Qgqg9Akqq zG*XJ6#VCwG6eoXd&5mWz3SD#wyVyf=EWLPSMR@6wd@-w${h@h(0oJi|i3x;|yI3x2 zms>`5^CGB_NVv(O1xBUC+{HT%cL_ss6wze@(?!}W;0p)C3tk|zh|22&3nXJlgfFUK zMKM{)!(+&v3p-{(rP?T-R;FwlYKd~hY|GIOVeAuZFl-fSDWnaC&BM$t<%x{}1&Qfc zG3!Q`4`Ck|v7{si`2YxK3awaPS}tI@%Ump53(@6ccj1wTckJab-FuWXq&-`so-)9* zaJ1QLmk73?xQQiPAnjYYb7agi>Sd!U<%(t1v5NTKJ8tt)RS%RI70W)A?VFw~zx&0b zEDznY=9fRZ?-!5UKZFB#uo?)S0P%RZ9*;2k&l zJoc}DwDN(qSKPbmyPx~SoA-IiGnCASzjAZGkA2{7&+-%52fuu?177+peSRC9`_jN3xID=Y_97{3UOF?JN8X^BazSKXAm46FiGQ4(dZ+K@#ls?5{^aH&XFOIEjnDKM)awJK9}u>#gpT6P5{OOKTW z;-Vm}mRE#ccFA{C&B)c!z+mvY0RA)zn+g|REEYUOJ|k8NfEi(yGrTD5>4r=}S4#9I zVo0nw)#Kg}=p*io=3;_X-!kb@zLk+GW2?aqQyQq)1&tHGOmu}aeG!R3_)C?SC||$e z(_i}RN6-4;NvECsmQS4hzVknE#@~PH%x_)znTx*uxvMYx#?9A%-{-NH-+bk|Rg3Ri zx9t8qZ=GKLuE8PfE-qJ!wrf^PcLf<5B_2F`;Bj$vc{0w_BeRbPS=2;|H8n*c*f}z) zELebxf|*gvx2u@FDvYI3E)-rYfBgNge7SY`vL#+gVgr_T)f*@VJ56 zEl_mbRcG#)vp>11+xDP4siCI!u&+Y+b4K5x7TVeF9W8HLJ@)8>&NR#J4F_L@fAj&P zci*%2&bw~E$C35^jIV_IzQ3=u`#&SAr>9&*AFtx=U7JFwNW{-SH?LIi8Y ze&W*h@qpQ-lcZ@{0rN5oT|#Fxk@-jV$t_$+>uKEx-I`!-F^!5#WI{JnXRPU%f)iue zO*gJvvr^Y-;C#0G@}FE*z$!HGPRQHJa)L5t~eyiq))MZjZ7 z6kQQ5rWKQeaJ;0{3DAkrLGM7DLd#-(#=PKv_VXWiLOOpaKmWx~%+9PI``M4UXmy=$ zt+4ZuFN+pAW^7!XZ@SYHxU-R$v+I|?`WaYhw4O6dH+#l{u~*+#g$|d`S2QcGc_G{3QQD-ta}9Tj`A3aZOER=6pYCr+oU^O-m& zwYpv_oUTOo7ytNEU8iC%4xQ?Zq!TijtH9alwCqZu^NI(dQ92`+Azcw%4CT&(-xMT2 ze?7J0it(IN_g=k(}I?5a|&{<+@E=XpM%j|IvIWjaPdIx&2l|Vrm%S4_- z7)uqKQTTD9X3@%l(Ye}7L>Tk+g2^tfrE6-2NV8)^UXrzjPzeV>7jBR@NBbOP+W-6i z`foP>9z@#lwvT1Edzsl|%zhlSeU}Z1t;mqJE0AqVD7H6T+UK+zWNbfUH^pwx_M2Zm z*6ynf55T{7AGp(Rd8vhc;@b0N)v;gq@`Yu^vQKFH11X~(Iui=O5AV9|7eBt|mp{Gl zk^5Iac+ZNTKD>@RaQAX83;FSbcRYOGs=LuDtfK`-^dQ`l8)syrXbKJ$$CD}j}GN>rlcRBSrpYFHJb8c@;44xkcL zEmr5Us0WpO zMpc?Za|dQ*VV6%8s8|K9o;9Qkti=N(7L44Q4m%^TcMSU4HFBXzUPiqkAU$?dqgWQ9 zMGV6tJ+!C@1;8jHK$8-VU8{?hi@D335$0=O{*-$8%n!WleeZeG$Id$CypNw=z5MNe z`0Q1ee*KmkE?sfURem4a8S6wP6$P#r7eyD2OUz~B`qE}m2)G!#;uv)+62o*!uCI zK^YxCuG%DrBW#$2l17<N@eo11`HGLw=?|<_PE!{!^aKGusYi?V* z*xMuQ9fGE56kCbKIe2spVb(^aWgH8hmll9T6*BT-xgWXFOmAdvL*4$|$lSu*)^)o> z>2|vjw@INw-ujF@r9nmlbs;mtm$tT}z+%X1xHq{QE-126zbNA5Lzi16kM2;+g-Ei} z&!oy9*c=ljfK69*N_ByKl)YB_G21o*0LE^wrKGJSY5%77QL3n36(dO6=~bAhWe@_x zRLUtp0vKY{MxK#{qgI82-0o2!qm>E3KmW_`vE8=(mRpuCuB>JxH{W=jMX5vZ7S?-+E-XQj#fAq)#BpouDS`?!3u(c@7Cg95VjQ&b2vN9JQWvKcbFdPnt8J}pEr@5n zq`6xJCxfZgc81vJo3c}=8S%18^em9R#LZ$U%oI{9RTe{521yGdmcrcjuOos1l`g!x zwbhWeMR2%f``h3Bl94#{<~1hOMC|JA@H09e2w!!c2@hab`D(_|W&@?!voaqbNP%Q1UX(2nG}AkhYEM!MKatWoZr z*bWK-5JJm7B1Q{FtPenQiCL?wO{QKP!)1jPUi&- zRwkvvCH#okivwmB(M{P)tBaJyyo`>NC@l+y65v89E6h{!CRormB>(;2{`JP|uC%?s z=E_U2zT*3~J@y!eY%pvy=xxkwKiZ|Rkak31@agTB>H|izSsP~?Mv>u?W+4%U+*i-cdxkWC-*M-?pHtRJIF^Z+V<7^Z}ZxNw)e}} ze(6iG?2n}s#cE_F^K16smaZ<=9=7N6j(Eiu)yvh<#~-}on~&M`UB~bJrlVhW{6V`M z^ztos+Tu?(dHR1`FYmDJ3)IWoZ?m;J_sws7!&zs1fb#Kk&QcxwPvvUl4}IW$ES|O+ zQaG!i&;R7d8&XrNmwkcuYhU@22hEB!m9jPk#$!`;oZ|PrC%xT29anf(-_ezI%0o4* z5>Lt5*M1e470WC}4MA!dRh>#pgu0d0D#X}W)T)MfDMED>ysAr8t@BDbDHiZmKe*(I z%f73q#K{PNR(Xq!BLx@_UuCsXNM1$_1q0wBs62^Lm8>Bxdy~?wn%0H!*w;G-sDvmD zvtZZlu`-hC;*lo{1};6DE)d;Xu`8~{GFk67h{fI#$&*+#LqbUM!Y`hyUjFcDe{;%7 ze|^q}-m6~z+^5g_=2y>GM_+dFR~BFM-DQg}Uvt}a9xgjwz4=oe*VWgB;~KH^XIH9p z-MI`^5G;gJO;nAbF6bKM1q{h+h_0fE3rht~mmxhomr+?5qPv6{1ybJyi=VNJ*JVo} zbrV0q8cj+M1HsRQ z$YD;sY?N=|D7s=zkA)#ok}-WDvLkGT%Z(8UDTmfG45O5^;p>)Ob_^*aB24+P8vjwOB;kW-N76pdb*E>0Hz2!#7g-cOPhfCcO(*hS=t6Na5ZikqW3(FiZN8!pX zUjT}O?jGdUOBfa!mK}RwriNO#%;TwPv;mk=m z*&9rC)t{Bp%EV1sdPdztrQ}xIF2Y>c-G$XMRg2w)n;qS)xw^m(jo5Lu%c9s;*^en; zmBXI9c`&CcQFruUcjc;EtC8ES!9%J<+UcoQh#_nh#Q<&FSgKR(^xEu!6}=X|pjwvl zk%=^LmIg;VJ)AR-m}kFGY}N`xn~m0?>Tl-g%s%o^-Ws)|(=}w>S|2SJc3v|}F&MWo3dA9+RRI(`PYR0Me|@PS6Df`GSHx zek~qb;ubdA+8Nr}M@|l+JZUKBH8QTCceE2e8mYmOqCnzkL?m>NraLSA-{)?62f z&!~I8Z0P_Dc_*Q8PDpG{Ki#%}T!iK&G1B(Flfg-K-St;lqA{cLuSBiBU@-3|je|b^3!#gci&AA*7Z*2h3<0K4~17@|1&4j ztT@Ly6`f`*E?9u^LgB1)-ZJ*ip>8K<=cLvsaT?Q|s~kI-J9Sx*BjiW{(*ls`6ByD03Pc&_iFe_8#b~L8 z;0vQ2m?DC8QE)HqFk0k}EUy3@{SrAIb;+BS1m*ZfTEjE_n)vtK{%Rl|b-rhdn4pZIn zyI*_Hab}N1+9TNx*(tR%X`=!#Fl|NbMkrkB+P{%uY{$alnYBmE?2xFkkFI)IMXWAX zAbZN3?pLxE%gX0Qd$J5ee*UAomBV-6ezSV{u}ALw$wO;(@44d^RkP|t;p{W6T2Gb< z`~B-~@?&4><#o%hxb6DysF&}$?b0*<_VvDlOpZHnyEh!Z%bSjV$%%*WeDuE0f7Py= z9Q8_n#Js(7*h6GBvhFeazJMIQ*Ym5FUw69`@41_IkmK zxBk;-Z}fP*yrIX;aE<@I&77 zH*fDT@%v7Gw@1&4Ur&&YdaUfz+G=MXy;eS}m%nl01%|4TmDQd#v-n~#@o3dyRqpu~ zv+qhP;*@*33PWY7TF-M{qDxmlst^^Viatigpt9GA;m@5FfQA&6hLqJtZNgCjohOo)G=;~IqRm71PWue!_PXw*zWkiLC zi&PD1Whirw3UCpH&?Q1QqvGmf)>MO<^BdmJD;9~50 z1g7wcXGCES%(sYL%b3}H*w_>dxzfE9@M6TnLeb?858N~<^F<|Cg${32TMkVDEs~hE zjHt%!;EifAFb-= z=X&a8Tak*OZs<~#kBu3nd#gJsp+kCXYUj{?zDc#0aD%p+ve&YeYSWcoaZ%Z857tJ` zkm}?2fB3bB9dsB&ZPF@oD@N2K6c&WYZOq1=kR@h-+%dotU7YYL9pNA|R%BD9uc>gJ|L`_HvfOUcz#J(tn%}D87ZfWq1P_LePD@>Mt!}$(r-TW5 ztZWnO%Z5r?$u|r0>P&Xd6BB_H)YQg~uGz5wbBDKpPN#NdKA8G{{Odpa$ReZ9brVQS z=RF6GT$Gbm-fpmC1ZMgpRVUK#e*Y_NWv_LUIY8$OSi)%C%$l9W+IsVZqawm-zzEQX z^C_zDP7h~bM*%sQ!{HQ~|9si%;p8YfDzyojIj?3v(aPSj%RW&6qG{ctXI?)8G;^y|{lI@O))JGTrs!Y@~3{x9ft=C0HbQoWNl4S+pDp zmrnRj9vMonb&$MSn`TNYsbz68WH&@wF-G&^(mI$pPF?n1LahTYqeJ*IS{Y|4>9n`_ z6faO5Ef=1VRf%bN1>*}wblFR_9Lf!q(a1-13JiS5jxWNHhLYS%6}ayFAr?c^8L?ny zVNcg3d?4-U+5$xdFuFZQ zRvjyriLMW3`&n$3MAe)Uz=yF_#Oh@ald?Z^1efOFh9=&Us z&tof@{ROjn`3K+n)E+xM`@r3wci7%rAA7*IuRCN%&zFxoU>pA`b?Ba(sf#^XRuQX? z58dOrhwT2Gqxad`Gi9I0Rzb6qKBwLD<@cVr=xxXDb5 z85^vxUf%M#e{tL~M=F6&eeX#MUiGi1%rD(#N8f1Pdym~bSym>WaoVY;o$_8oo;Z7s z>@(P>zyB0ZmRSJQ&K&#lGO&tQMfdHmUwHAizD^X08g-lptjbuTLR9ejv1p|qMaAdP zSNVde4e2)6%2lJPP7jOK%T>p^3SoHlAZ7(4g%_PD8+G|A6)B2k&xmyul5(J^G(DeX zM*_p0SDY+$v4H`LE>N*xWW^p{9SQ)CdE^5i0d}z%1H;H%@k;>cWnS@XYV3?7is%aA zo+)FKqM~tIBv8Df)=Wy*1%$HH^X=CGnRbs`*l?94TPjDIDe2bmGzKGG>QIrDYUCV!Y;v zlt}Jixl1eo4uGd2EwCI)AQ+1-<9K0g3hXk0i9}m6>~t#+HoK!==gkT)vn=d>evRI% zZj%)hNnQYqbEN3gql_6+D`IK1EUnBK3f5d^l7C{`KpD4Y_Z@ooAor&3T#p0(=h|E> zBxNpPX|o71(h!I)pBA%}=k1R{Ud%u&W+Z=+dj@)$D;C?g0K^;D98-~GY)z3df9X=jv1Mt)pPhv(_(hr zb&1t>8P>8mZ7d_9tWS(O7DEDebQ%(Uh^nxXCp>@yA)i*8&H@ZWD3W9(QXTp5WhtLV zhYmk=8Ph?_T{k>MmobGqJ)svUW8EArnxTX}05AEB2~t!6V|EnXsU<>fE|GW}p?Y~5 zI_9XnTu=&~T|2;$ge+X-#RZeU1tbGGvckitYu#LKjs-A_u2aQ%MV#}5(q-$cb`m-p zT^Ij9XZHcUT~XzG9?3b!mz;A3$p|7DB&mP_MNF6wL_ondU`}mDf`H@tt-jdF;0{N#B5Q|dOdFo$u7(IcWO3_Y|ZLlv6ve3K>;8N!rjUJV&0X@yTiBPBpS zn;S3Mb$jSZRoXNhe4LTUloFFGlGN9zs@U0qub1IoGO0pLZW&KsHyWUZsa310TLVII z+8#wo6Kd6JrM59*a%{d8Q~g>NqTC^141DBcisi9OT?mJ!8BVvh)6Oi36+^6%p1 zLK?y}6P|=p^IKo;|I0pq9VoUd8(y})3^+^p$lz?`J+a3ok29jIhveF3DDpc#;ujoD~@ zsVYJA+Ts`#4kZL=){0^9tiJTf?8bsg4CaM`rJ5u_6(u2bVpPdBOkNf9KwyL_NvC-R zO*u)OQl-;@mu=P7T*8@H_2GBgl6}@`uX)u8N5jh&mp}cncc1^64_@^7k6ix6Pl3!g zU;D*-Z@v{plpwgHypH6cGOmarTX_#)ORp9aNgK7+HF%zTqpH7BOuE;49JSg2~c zqv$NGVV5S7iltn^j4TNW1|B(FYWS4QH`P3adS{6)_&GPylPeO_BY_gjGlsC#SQ-vJ zE7#3N9orWJszIOF%ey3eRWM0x@Qi??mz?4dSQ&JF<4#XD7lVE zW>^ZV#YAYLVFN+nNbJ#Yh-pQ5qU!0VyKYAAMBQ{E+_~J%-QX0*-4HZpn1(c@gigJ% zpDT2oZNn9V$f|~uJq)OJjA&xg%uRoG&Jw{`cUy$G9Xug64DL3{<^T~wRd-c4)<&nf zkVzaR5-Yeu!w#^9AWfi!33@;r@-PLyCgERv;DwA<0l;#S6c(PPA4e0l_&78xCZlRB zWJ4*@xRg)qHwL8v zH*}sMqc*3J6gvTZKmGBK#Dv-kGx*Aip%u~;;tVI#&;)1>Xe+AWPkahhm5fd*;>|N? z$~#3S z2M7;*sDuN>*uVPOPjpvvswk)SpMU&A$K9c1B# zM(D;sILd6{F@7^T8Z9+kk`<-q~QcrWK>m~X3Y9U7)2*C0r*Zr)LqJ_+xDCx&geyvo)S9yGeb@}o$OJS z!ZEt^Oy`8@$UZJL=~W}2C4;&nR5!BjLMn}=q&vxb;fz%m)~rmj?5 zlFf7ck%mJ3Vv^BN(nO-!5SYh@^GuA6j$BpaN+zG+XvGPbQpbH9Q?1Yh2&nKx8p3~$R7R{LyPA38-%@KMeNo}Uq3;Cgn{uI^)`Ha*yF;lUWDh=z7K=x-U zx+ZF$sVaH3l+am6X{(j0+LCmtc0a?6MV^)G%Rpu!iW2rvrdMvO!fEm#uO6oa8j786Fx6$zAzbHp%3K2^z!wbC|< zmXaj^*LU?}}UbeUl zGC%R)4gd0;2OqiPl4B0sevhqJ^8WJC`)zXY?rZO|$>Q6s`>Z{;TI!J9*J$^3h}g=q z^<}78acsthm(h|v+s7ZY6}uT8e!XoXdmnQX*{eE+A#<^OpgXtKz&CNDLg^zxU2z$d@@l}8J6g4P{&W1)N6{eTBOTASjXI^guvPUS1V>#%B*EC*U(x(n{Aj9814q z9l^_9Res0muM01K{DWuPj14SHmY6|iU&*#5+vW$)cI<+1>V-p{XwH}7f4LI`2g)M= zM}!&zDV=VF)xv$wSsXH*x#{WbL*G941~N`e3R5B{88#uHc^DAUxv4E$N)`E(YbD|1 zka0Te7d?8+cmfO?%}J7}NFc*z=em%l(P)~7or=S3&Cw~zwysDx2&s-3CTS@B4ZK_v zW06Ml)Hc{OpmjPd90K@KW0%vADh`*>^8Bxsq=uAa+w7b$4F*zoax#i+K+7pfEk1iZ zZy^s9Q){Z&tI!ob=Mcb@XM^VR3^OsRR1s@Xn0l#G4M)-ze+VOTm$pX4!*cg8H=yoZ z-HhCK+?epi#;w|P2gG-uf|sc^G)a#XHm;t>>Mp~EAF>UfWJV5q@{i)+7>>T?o7@)h zi;CG6LS$#R*K)N9kR0nx?z4pxn8ZX)qAUqflqCRzU+ilIG(=fI2XG3s{_N-fV)4#C zSa20nl_nr?0b6ib`TxN+y zeX-L@aZ#%(W3hApS9pq;mV3DEh}zO#QgXtK5Xpfl8K7YskVsbs*b`b)aEOk zFbL(k8+ZgQC9esBTRQZJjt;*e6eZr9K4gjs$Cs1g1fe^ziUgdGIN>sM0u}KUW7eY_ zv-n?Iv|3E5z8tkNQFT#URn668@atV|+Sr4ZvVYL?m@%3Y(<9pwKq$r}liJZ%7cuc0 z&d@W>S4FavB$^VrBK^Z-BukoO8X5uTzLfrPI%_2ntE17-=|H%?2%~c;JtdOWHVRAv z(|DNx&S*8d&?-qfGd`0DOk6YrSGy86qfEliXn8XYA`VI`nz3>+E*;JqGA6%-V`fe+ zATy8Ph7yw;KUH06RY|6FP3o%?fdN^_Hg0tIG2OZt^Gz9Z9OBG-P4DCq`klswA{p`lB;)n$r-plF-uVPTMR;3=PCGV{|m#me{m6 zRAmM}Iwd})DxmtqZ+~IWo=<)$`gpYfLwq7`5u8Xuk&tg3VTq85K+6`G1d3bGMPfne zdGIo|+9pG6o>mqA>AO$-`1{`ghhgN&>&f4G+(NTo)3!JJsWs)PZ-4pH7j$ehc3;Cr z_vW${XDiDR0!+5P42(=r^UV74{E`1m-pXg?Zfw8|A1ZQ z?7Ha^JFLGDylhLhMPx7;9JZ8DMj%`i(l_$K!^Jh33eE#ewKMnx*vNO~SDYyD8Z{4#MPiPKcgqLADs1|62 z{s7W|9*7r^l!t{01cyOCUxAB;4K)E~5gG!FkR$n!EUaopSdkK2zO65l9Jn&II5hZP zH{c2LVlvM@Z3@w~)m$Q=7^+oE#>%)ZGSw(F#HUd2m~u@dJ+fKC)Ep`&8EV#wPG-|j zOmp@ppm_~>2~9?kJORQul<*0nM-BAwK;02$8-G%0bYvj(iJc$$htK`vxxS1IFWZ$3 zK6`Wdi|2jR%JL0YU2y%C=Sz3oe7X5K@bXhYV`|CLiQ`mqCW5+_4ud@407T<#a;`~a zpd@e!C~zu~fmWtrh31NyJN2C78qy}=qdPa9o{E7T1WDU^Y12^v6O&{KO`(L)kcBP? zBwY$9ENROgR*^xInbt<~ZzR1?PN6*W=p2KhP!;8BDVac_8F>SjvO%s#qZr1pvmP_M zCfcS}6#*6w`w@zm1c+sfoHz0|dL#lP(Ja-M=BaJ$_=qlm_{qrer(Ig}#L&4yUt|>H z=nkJY!)&AusP}8AMX->VOL`7zAUFo)NGalH+dRGNCH;upTU%Z0rE7bJY1n}viO$v9 za#sdGE^cAncieQ`?Bs4?_+owu;l|p13c((#xR+W+dc~8qIOd_Q)#eAeUi65=wh}sf z)aIFF&xsBLX?~>;Y6ky4mb(sObnhGj@&Rw1|kZd3cAq1 ztXzl%&WNez-g@K5KK`M%ochL-UiI=Vw%p{%qYnGvht9RfRXirH6M%{3^Di0V+8OTzJZHC96O^PuCUxJ==EQuBM&<`eCK7N zqo02$=WwYISI(s%o+g4Z&ZkGBgiapIpK?{h;hf2(lY{c~+Ij%Z*n1jaq&y?ym1DLw zeNvox;d*wiulXi!%#-w&cs1p^E>y-ZmZo3K*~GAS+KMo2>kB0Q1!;Zxg3o^jYLEzU z8b=wgAsA!xL~dS5I6T$>O(bv_DCWJ5)?eR^-UD<0%Yc{XthSn^W?f(&d(4s7UU`{S zynM(%y3p2h7}olX%JXj$&gk~f8u(ljtxcQdr4?XNa zQOYW-uDs<|TQ0uD;)^b};IhjtyXIPR7F&FwrI%UU6O}Jr_C-kC(;nWY#DUN@ZTw^U zITm@3LSk%iXjGv&S&z+OGTFm&rxF)A`khCL{FZ0r_(sZarDjD;w1g&~5_-@T3zZAz zr#a~-$&L7R8XS|WO;z>~BcN+j*YgU8)q%rId9_-Z5~<2;8VJykHpDdTWEohJn1<;g zgJu=rBV`_@T6~gv!7QQcWiSzB26OP4SE6LP7R_2`+_cS@y2IpHG&?1Pq+j|~5y+$z zs=}B4zpgJ+!btjQrbLmFdW^#`sjl&jLe6_?b)DG=Q%LG8MwnVnMTCTJ61wn82(!)% zM3|pwwFr-ZY{^!vtyzjDBRO|W;I6CaH0XtljU1Y*s=Ma7t9Pav3)^xO+DIKr6Uik= zj&BqVHn9df1~u)F=|>7F96qNxNey$FUK}I2bTUK2jDx8*vuUVPIf0BH25?KKGvR50x+AQ4rpn3GJgf9I zx}&g8QzEYml9Gl)AkHXOMGyUq$#rCCs3F2}6o*KKH$^^z4UvghMW7&w>fFemSa-g(uNkKFdHM{oVcLpR@k?Rj_I zaKQs}FZ_qoUh4g2`>}1t_Q|hQ+dN9~L_Vy}(&+i@-blUj9|1r`IA|K9qt4Odo;y`c>M7IG}Q-~w)uTW`Jz zunZBewA|8enwxF3J}hiy*{vBg_6jp$cU8}~X|O{Z;_f%iuyf1JOQ=}Q>!~u;ZozkWwhGD9bhykRk2;j3{E2bhy!pJOCE0NTEY{n)a5zrjp z)^0djllTnK5WiLUkT3nL)r9M~NJn90%F%g%HjC+$SXF=dOOGW$mGS!Lvh`(<`Q($1 zeb3pa_&D~3pZl<%GWWeNE6X-x-*?BgK9N0HTz=#ZNC7(NWO1$`oF-19Xt}UINQhAD z%&kdhlcMIGV&hEI+=*60=bDpIF}11?^x&ga)w{M*r)?)GF)|}5N(9oD!E(YQWi9hS zr{p!S6-ieNeTC_|R^X5{x@=HvbaJ$gG_O0F$w-LIGdij?&k~*DxT$0j(#!$x(QEE2A4<_qB5Ir)b}O+3m?as~eMBl8)$PO!iR7Y^XV>)!mT6oaVD9Gg#6@ zV!$}cRk@qH>vjLtFRQvg8ki(7W>xE-ZTS_J222)QXn{o*eU`N9YAagBgIG!0^-9u4L6MkNQt_ME3{Bwn zJ8tnSC=s1rz>qc9TFt{)%f!&lF)uhm!=MT=@>=1D13_lFpbj>NeHgIqc3ZBs_Uhr~ zO*h+c<4xA5YJ&~eS#71|Z3f=&IeXr6!?hMPjSggPn1<{CNt~7|5aMJj%UtR;=Qo}F z8bHq@-=!B{#Qn%OGegj@IJjUA%)FT6zhe2Lw54$N+;;N~^fMbcbupOcj^vi$nbKiJhmt z_04O{S;fKi+~dVBc@ZWcz+)`uE~7C0fNOC+VN8#1Zo2upOD??-&2{@ecQ12tx#gFF znx#GV+~q?be&36ZJ=(5=?Y7@)optA!W6LbNguM{3Ij3*sY|AF!K7_I z6LvJYw}i%NYIVe$RD2?}?cZgg@Eaj8yKC=7pUW*}TD=PQ#3Gn=`$<7g~~ zhM9b>!%T9vCx-x;63LOqw2VV-9DV7L0j9cRib+pLQ}1<$Nrp2C)O&5qvrw*~+D(@Z$3iVUWxPl!t`8O?D7JoZ|9g;@>}!^LPT&K8=#`taSb^G|>D zZ5*h%x0``zuPxhg4Qp7Tv$CvcW47gHo3Zg*bVewa?wXTnb@9QgG>%+@O?Yo}UWa!wka$m*{FYmeKa(=-KID^uD zHRaFCptRg4vK5cncXR(=wjX=W3JWg1=-8F5rU@UM4N5gbbp(;oac!Vy)q^h7=Xfy(51aCEoLm`k0aR)QYfke#kBLPLK#;PkB z3Ux<=9#s*5RrRQqDoJ19DrBlJlI0WzQ8lcK_=K5Fts)s7&^feD*_@n0*|(Kb?8JW1<$}hYRGAlfo$zwo>bq@d0WH zB7g<(gOi;;ikQwj#c>v+oyDDl70hHBqTM_TrVNAt^hd6AQ&%CrySj73ZZQmrRnPuCRO}HSkkW?NF+}uzFP8< zDzv;2B~PIuW@DMwPXLpKlC~wn63LNLmHeP9(oiBih@pXz_#_d0(t%Kn$uLb^GbKzz zP(I;)viB~loP1Htf(oP1&1Q5+b1kF-rXoAiv0*tI^ zqI$c>EyjJPyIc1ow;l-)D_BKO->@1x&7~Ydp-$(UR7s;18BSwNM?B|X4n5sT^~e}j zb#ruoRLne9iPO6C5*BHc@Jm9t5;2$s{3$|im;>j+R^ltc6+-kShgFwcYEcjsMt;_U z&w9m)C!BfKTQ}Q$BS3YjrI!R%-t^{^tup`a_rDT}0X0wtLOhl#v^WhA76BF|QfrwQ zUS4hWmFTog{H}MOjS18U^K^~YL_-?9g6eJM_10gDJh03R#y~(@ZN1rs8?6f~EwSVx z7Q46IaZ1R<+2ww}&cn$YZ% zS*(Ivb;^Eb!@1EzGvgg!$A~sAr47Nc@^GK+>l1}u|NqhVI+WJ&HK)zyu3dKAZvA!E z@etcPQmm7?tF7#I?=giRVcdA_RrYDq zzv@aWt+&=1?(!R~v*tQ$t|_mDmFs0Lv6AGC)HF$%>hQACtn7vbH?^shu8s^z58AnyJ#egiyp} zDq-5>iUaz^sq3oLW-4NKn%!d@k_a|aB}aGqpmmWGI50D~rC!t`)V7fWh<0uUfe~iA zdFcA!%Gtx2*?8Fo38w>osgoTCG^WSobVktN^A0p1{{6rI)r&I;o9a#(!mg#{37h05 zc1p+)$j))>42sQ0frIEE%FzuOp$e^`gh;aD?3{c}rdtuqjhG`5!)zv?bEdq*9T8pI zp*90Br*)be0J1~V5IRQlOtX4Sz(7jFo(MX5L+@i`;CX@`kB-d58UYE*zoe# z?!W5l3*W!%7Rw+0oXN+&yuS=A`#!e+NgcV*x{$ILmIq$8tlU?>2PlZ@127v>`-dXmEk0TB}NKw4um8dN?*$@p%%Pll})a?1QV8+Hd z94pc8vk>=W>-D+sw)*VlWwacE_HAoRrNHLEo1mW;hl7@Kh%W#O<-vMzA|_M_5^4e= zs)Yx^Ly`zM7jQ-ghpnZOAxunFsz68rn#HGYKtsmhP%F_u4@vkm zS`p1i!~n(Y(W)Nfr#Ull)Z$16h^b*18I5V|iUz0)oOX#D9RtAm)Up9l*Pb@8HhE3p zWnu(0RCVf`PfiJ|tR~xo4KJVcvZDZJV7Z?!TVnS9vX6bi%hs2r$)HTcYs>xf~JDpt8f#HkmS zB#)^Uoo4bGnS6XzY`{n4EsyAd`g|OhD4hNV*$|J&cj76%R|Q6b(!&LKX~%eL~7Ydbm%HNg%Osfmo{~ zlmZhlt>rDd+>(ImF1zly<4)VY^B>-R*4s~)@3#BS&@QyH@FEMWzQ)QB^#A#P{YAVd zFe`-+MQ(s0a3n&rN{H@FRR9Ojwq9&i5w7t{vhKonFcDgFD33xQ`u!hx4|yPn87r)~ z40#DYdhP2@VwJ^ZMGKvt!CE8sijd*kbqpF9FK=|rY7)~_(wwswoxS6=lct493&!H;z{@w>c#XsTs#m{a>uoka_PAqS@WLa_C=XAbyWidh zW!cMY5!)N4m1r9h_{yM+MbQKUbuBT&+KVr-uz9%UR-2jz9>Q2(zT?ihAO6VufrWqk zFbic#;wbVbdNMbjel;>CCp!d1)B;^WvP4)SP$E-G6#9?z z+mmC|(@kiQblO4GmWH7OGl9&=fH7w#-g_Dpr?ci6FsfoYG@e?>wQ`Iyc(fu@!YR#T zMxAP?R<4-YiaN@?n&4y@rjx7Q(PJQuuDOv~O^DTyatbr1A)qy{7fOgpX+z-4M=!(d zy2fMFf zTdGr(AZ1m){WVbY51xF$_rC7C`4Zp9{_;In!^;oedeNsp@Rl7nUh3dI)<1O5b@zYv z8i((>&S873ec&#u`aZS=Wgo`T+l-CV#%!PbvJLyV1Gjv|5j*i*}dn^Iz_& zbjaO9eK#8r_Kw)%i%hm@dF*V@w*O4E$_y!c3O(?$eZp{@1l&P?u%UHNxX(H%ObYaY zt_Ua+xRc|z@(Tb4T0@BNW=evG#9CC9rx_5IXw!=(UWF;W#h*+ZUr>;!&2D3A}EjSh!ANlZnxG*FJ<7P%_MV4``6 zHumVz!m*quefwHlU1TsLStZq@R? z_OUNG+Q+{9f7#2+ci(!IUD@6juuTFi4HI=b6!|+ZIujLPJ+A`;GHBW~VI}d75O9lWzgeDMy9nGcsqD`$8M*SklGfYa7ZK;(rgND@749bDn44?;dK+7%^ zsiFj5Lo#7KL^?Gg6HOpf8&vWnOBpbVSxzRtCINf`h9Id*gPafNX-+IoZbv7NP=%TQ z=c#3_vFM9%GR&yE3>$iee>j#aP}me!GFPkQ<7wG zvbiS2aIz3lEv5s+jE+WB$+x&727b;4bAcOu*E;VyU}zz z>;5%6ch_^96K77q#f+=>2_PHIbq*(1{K*32aF zb3t#hy0|wugBUbUXiE-=S-uRB;n$WD_!OWMg9YmZRA|6N=;bbOz=6-T_P5QpTb%p8 zcfmMbIJFTP+J%&@_PR&xf55&F3MCTsS2!nBOCgR$PY8%wVmID&t%W~$83YEG7G7jw zcv-xsYYl0rYxoFZ5%!>ipJzFHr=7QhkMyK#&nAe@f zYd*$qm?9-E;wyeL(gv$2(&*6v4-hQv zG(5;fr2qxBlt{EmO@n3vfr-q_UScB>+zO24dBbo#2LNOF)<}&JF&XL1qCYqpBrgxt z>(_A5)jEg<608fm@v0*Il8!LxEU31@+~*r zNO7m_wst~smtOdsLQg^*Ye?BFG6%e6>?qqj0OX3^!Bra)g0#G(2pVvjFJLPojLQTt z8GQE5BbVpwvxl#Hd0`pi_JjvaAa6!J|A<3u_9nKsMOE@J*El%^ZhB#xcqBYbUkyTluVXy*Ut$4eTJ25sBCZmcN4d}WcFx_>` z%p1la>NGxDlWLP68ZiG?l1??0Qzea}u_$IC;{P{Z&Ys!1j!KMFtBV}MIcQ+n3bIbo zwZ#Em6dSC=VKPh}I?c>LSB7vaBD!g2mYSafEF(J8O=-^$yHvV1B3w@Y?Js{K|GCe9 zYWp3wwynYwq;~Jf?fmhAKcpPi{`oI{V$OGk2b|$(^I4H{MlzU7S!jS++o2hG{Kl2@ za{|tgul9dEe6BiFVx`cg$Q zzuy#ML=<~;{2X-pt;~M1PAo>BPXa67_?a$`l}YW1N6wiyz&|XgP7nU zAW08CLRJ@)yLA6?Gi<$`}FMd-kp;=~EUKt}M9uk!&)m`+Buos3RH;KF%?=*%53LD5Oq ziAFxe(RodSCgDCg04rCeEo~`G9*3UJ^iJ?jb{xt}(V8={O%=ps^NCh}ArA}$D`+Sl6A+eyA>uS$SS1nKo${79bWfoQ-l59drT}#x_!0-Q0Zoq;MmB&{ zesQ4DmEl|(ZmtX2@&tAL*2Hm+Rv?lA%!W_uGlynHjWN;MFBdN`w6UlG; zrSar(0YX?S`sD`>v3U}c&44q97%3*_ShjXVI(FgW4wJ)8Kg(IzZgBjGJu_%-lYP>0ib3F#r@q{HdUTrSfn-^8EG;& z?hdHqYrj3TYcn=DkXxFKj5lEnggidK8Kxq?efl$>F!5}+*4*5%_rSzhaG?bN2mbVJ zx5 ziHc_YP&k-1)GE!6vZM<|OkIplM<`wwayp4+Kz$9|F${|n-U;&H44bPRc|)srnrEl% z)V!()OddniFQ={U@Kcp1r%fd`-!v?lhZrM`KkTFgJ)Aab8);Qwl$33znTuPm52KrS zCR?=GXns!Pnx>$}j|uoSq`L z5&sA(gc&3FDfW;TdsI8(5PT6-kqkuK(h^S16BB-mQZOZiaEKDLkS{Td%NkOZ^1kTh z<>kj8y6gTsZoK!l>mR%Sc16q0L1s(L(6j_cKXmskHnR4mFN??!F_;V*`@NKfW~6$?t%iE1N?S6LVb*ni@RUGNj~PZDM=pigb0a^H zX9If}PZ$xC@-(nhryqU)+iktJ2m91Fyc}w_(EN!HopbRQK4yvelJh@)Ak)xPIsXfdI*dvG$lD) ztJ1XnBz4iTAejctVF?;jLX55lp}VFBzd0^CQbn>O!r?e*9KE22+F?oh>8T4vGIDJj zi(;*4s9%?N-Q^kHqn0*J)Q+(rc$lQ2X|8Id={uJgb=<$)q}-}X(>JUP(*-@$j#Ss# zRxL|9nmR?3tX9{JOR?1O%exi2Ek1Soo4L_5QVftu@xP;O=U_>aHrC?M zhwImT2QILe%e3d^d>XdCcv-!*bVtJd4BxL2i4p3KvrUw`C zm;hDcQcM$ru$*wy1HY+=IZaQ?5SDOC&iawkqsZHmA_F>_a{VSb8fJ_hiJ3@poyu`y zGPv##wbdkr)7sFwGZx7atS@;@5csB(Dw6W~rfg1m8VFCzr=K1Pco~xO5DccOzG|K; zIJ}}ebvD;O!M58xzF6~u;UJgKmTA4 zJiSVD#F2+m;-iZix+v8VZS}%hW;1X21i^1{RI$I5uLi#&0rHp?XXh>{LFVDHE}4B^uDG z3c$Rl+>6No@&k9?bk{A{z{^&XTUG{~tvf$@-)${AlL3}3A_L2o=ip`Q%Zku3XzUAM z{noZ6W_fm1TI@ zer&kgR<7_e8XFj!4YEipM7+-3|jo-fzWU`KQaBKj^_9eDtf8gDxUvcrLue$WJzW3!5*}Xl;{Gt1q0)7?Z6Lk#UQ0-*LNfSCXE(>CC(zhv_~QAN|zrX~xbv zk~ffUD0MMUKHVdEJ!YPKzDX}7H`Wph$8FKwvoM&Q3Cqb9_}Z}6ksH^g*D-d1psd+a=OT%uEW(*|$%6li(#jW@75?9CwK3b~Afh?B>-$dC)(c#>PtLJ?@x(x)v?Yp<~? zZB|I_gK|@IcZ2e@%I!7hz@QNQ_%s(0)!ihFv1MXr*~mbmImhDipZ@&2R)_?y)`rBr z)``5+&7|}@7On7Bbl^H(?Cc;m5NatDFgt4WqxqTlc$gcx#d8n9vj+}-T5UvLDE1jH zc-g0|JpnL7D5Q;kol3f3h9QW}@DY+)l0a@RDHAYrM87_X<{hi;x86bkVC=+hy6$R0 zpnEW2?x}dcn$oZRW&%_Sbs^WOqB(Hg^;dC)gJ>4DKY>FG!GjmWF)q{3pv)-4G)7b-fyv;}jZN3S7zQSIWx&+pPfO|+qmMiO1y&hLIV7c{Wpt$c~TUG|8!^_}vXxfVN6OZ2d&4+LG zy)R45!1DcbF9|R2w&e=X-)EB}_ua^rY+%{?a^L%!{2yxX4J;)?&HY%a_m_jree6pP zINOr_x?}fv{qcJpf8e(8@_x_WaNA8*TVbgQ)cqTH**y_1wk`|;pZ4ZA?7aOpZNah^ zOMu~~De&+e&ZU=F!fR7*&|tFlW&d`vD{R@Nma=*K!t*|BA=A%0d=uLeGo(z_WGD8! z->C@q0d4?k;1Q;S>|i|{2(-0PU=YxggPCDrh!0IAK9DFS4+d%rGpZs9Ck7bRqE)4e zn2$?KwogOr5G8q463wu-s$gbJ3Tb1foGP8tpbA8_{;PMwI?^`Ks+PhquTJs9(P)H` zGCt%>MstW5ffF=lYJ)c`Nz%g@97*qtp_XCzTGgF?^R4D&$g|29$lv;wH!xeR zPpiHCt*^DXY$@6Lvd>~mmYaP6?7UCC-(Q%ox$N`aUj~*IG8?6xd;T>Jn3t@}Gy68GU8yQ7l(?$XZ%WiW& zXTRspxjJ>q`!IfO<+`9#Vt^DxWiSoNsH!`)iK)_KeGLP2ib+zA9{NWYnXMK5h@b$|}j;UEeo(7VVp&1|#uLVqo;-rt@@QQ0=&9xjkvC zYdTp%1A?kJnT?jRN-axrHF?Ek)Mk=AYsr+{_2!#c<+cceNCg#czG&#L1U8lmjMW;E z)V)=nu$WEk1Q!4!Ac??C@CLA0p0ltE5ih#fqQ3McW_!okX9&qGXV|I@>DUb`FcYxZ z{wkJ(cc>DK4KO116QRLz0z!dVo2~&Rk(<@w(2Ad)5Kt?G)3tsDlf`x5NehS}W%jJJ z@=7mx>9Np|^=|Tn(JQa8f*TfuEBEC|p@?H-$edOnfe@ldjv7Q`S=mv8;5bka>&Ws$ z?;g4nZMxA$p-bBQ*}?W>0!4Z`oZsljluw64=%!7Gj}HxpV+|9{dtSH}<@(*QSGKJU zfpR{c=9A9e3#OC8G*HWI2d*R45IS&ZwVWfOrn%6X^6)ZFi$@M3K+4G$WbL2#nGuh& zJxs7}o*o?`bP-sV^MITs;(7mQwmaPxYe?DRa(LNxYpvkoKmPVt?%hi+x+wp=_M}(P zWA$09w8?!gTh)roEhh$W_xGi7{ugRe&UnYgJYkqg0{Nczh0k7m$pxMy;4s@vaL{j= zdjmo+Z)de>X>?WbC&C<)+lK1pcfTZCaG}ZKnu!WH+jyYX{IOfr3^7+tPSfVZSHA4O z{@Wkn4#$F97(lJuq~)}VI1-LTiA0ham|C$muuR@jC^>41de`G2=|rIQUvgCUD_&F?m&Lxe*^M#W#(0r)#t-i8e`dVkNdA;@6jHthte3Q}fL*8hl#!_Dm$}lyQE3!uy33OR76PziY%fZX8 zgl2V=uDgj36Mv|0xpN0m;Se(2F0vmy&6{3LaU&{m>k$e?LcD_3Pu3AiS^ zJvq%4tI}lb_s&SlYhqmWm3etal_sqs35Uc>;*)}?0v=%rLQFLs!w5^l%YqZ~swB(^ zISr)(8#&2h6=LEUMMQjo4Q;x>lqSue{0bm!yY-hJxZ}1PuDJHH3!r8@u_0)S%K)<1 zmVHCp=f5z$zwCQokTTQ^6qnkS{nTpm19L4j1Iyok@)0sX^plU@`JFG{>0@7h!Tgnb zuYs2zzWw40K5^E-%f62dEc@hF-}~xcQ9geSHA^iw+m#J91JU?WIQnIWZFl-f2cCA~ z{@!0ca-Yo)d(KvS?6Be5t1Z36qKhuN*fYMryv&k|9DKmOAmkfQe$`oTd&?Pbee<(- z+-9l87jl<;%}FPKWANtdU;E0#57{5GyZq7%U+kBNC!GMhoqg6@_kZqQFFNM=@b5pI zeTLkN$rf0B0UO){56J+_T0xi490aEd;?ozz7a`YBLg)zMlcR%#ayU^AT~eqPXk}8$ zRZYP1XFv7XPk!9rCxKgp@W4x0;5?+S%3f+ew^G#>aS;CVpZ-Tx#QcN99&*XzxP~?{ z6RuSo%P+s=BK!K7ZFDS$TS4gnEK68rXfeUn;4Q<@6$$Ww)dyr`Oy!(>x1ukn@@g8Z+P|b{>6Ou+fMF#U-oI+jO}}0K9TL`%bx9+oc@OP6w0ts zZRQVa0w_4h>FG>y){NjE;e-W_TFG@bIggxoWIES00T$2{N(e0KYkkpG_M@e8OoaNgr9PTH#lPsTEpoAVx|UGc(48BIDCZ zL;S%U@{~vf28WDPceC?qXXjnPhPUZt6<2UCVYaDaL*1d%fIlo8RY`JA<854xG-Qph z7C)aOXv@_4qSIm>O=GzOBl6;%xWDIsVSIQhDZ@E%b} zhPG}i!62{(^a=|E1YiMoe6bw0!f#m8B8Xl@{;ubcqi3PVzwl@`ZhaBM?CEG#ZDcg6)! zi!cR*LQozMph|*Lxgw^BQLUn`IfT;~&@Z$oq*lynebzh9ylPw$7daM&#?_93WbBw7 z6f``@ct_J=^`eJs-M{|$hd=wtj|^&cA39t5seR+iFSEE0gcy;;hn^?10tw)F6A#hwAYXAn@tIa((ZjT!OZuGB(AeRK@Na$5oda0V;n^78?v4r)j_d1HMI835MN;0vk>Ij{A zln-7jR>C^NwOpZ^sc)U@{d`~PVRDfCy%d)mMfB^N|PK&RSo5;SW+!or&QIQ zqP8?7k5;QnE-BV2njSJkZLmnggh|$2P1NGosV2nC3UkTH=$htP77*$ZCz))jT;H6G z9uLZ00A1^>4x3$`^_f(rB^ehh&njJ!N;pF&wfgcbg5P++b=BPS5nYeJ?Bek8`Hk0K z$4ZkupPp+Oa(K!aErNAM_`-mX2=(Q?tvBDq^|>!L(CLv2c{+2lu^1qlLmVatQLa`( z8Oi&w^Z>)A&SUE515L@Drgro0?DW?dXX0e zxvCKsw5V#KnY`=mZ+`i&F6@}8Qbk_qg;Q*ke!1J?%$-rEId<}GJ81W{_uX;TeRf#whc>(bv-g)zK5oy` zPTcQx$L@CYew!S$=O+8_xy7dIud>*}3wR{uMzg>I3ofw0!li{4@Bkls!L7I0c&F{R zg62+n<7)v!tCJw`K6~x@f}@Uth;d*v38aPN)Y{?;09v<&Ti^VK*PM3B>)|-b-34KuR0o%#0XcbYNT|^HK|oo1xFM8D)K;h*<_H| zpZDa(PIB}`SQn{MrAMCeFri+C7`1tIm@yAg=+eEo+oF4@`ygy9O0fXy#wvbxw{^!9 zHHpMTYaoO8UpyvGLks%_V!2xj^8Nr5_pJv)1CM$`0fPd)8T?|JV#t!wxw zt6&nCh4nxcy@Mw$G_w%yW1%gzPFwFsz+W6_BAtHf8@JtRtL?Yh{IpX}aer}@1D3D3 z@=`MVkGA2~N3-3neCWoys*u%S4V=Q9pgVoxNDVn?sk@v|@2Vgpzhc>I@7*Z0^PEmU zuHs=+(HM4lhtKT@90fqUj1ZnF)@vBmRq@3gRaJ)bK=iW1A!wSpdFUbM@7? z-eQX#x7%`!RaaPQ@x|AkGsm*8M-V=qX?wE7Hm`I=_l zd^b<*ZuNl01TaZjo%bRD2jW27@G>`=r5-_;Drm{k;VA3o4u_-9b)4qxGXZY9`39#z zFU9jSBF=VvFDplz{O_8i6@*uz}^zCP8@OAVSDZNY)XFr>t7g!>kKiP zB`(s+h&Z8(+s9>u!TBvn87u9!{aHUY~4XVCbG$9=R%>?DYq zAmysQ^$ijDL*ISkK`8iJkKgyrFW>X^NACiPAAj(+uRL%o8V7OrEmz)o)8&uca}y{H zL4WnZJ05@F_6P60_JKRE`0f+8f8)#7-FnT(Pdsk7owr)*(C4oI{QWlEfA=|u?78lN zyRG%SXRmqqb2jqdD9g&WTlb}}HfDR5**@*IYD3dLovqf3%qJYa{aaqX?`vPQt9{-3 z?Xv#fJ8!uDn#;k;-D~EHm#rPU-9y1aIk+q+2M7aw;9+c;B&?RfFuQdS{i6Nm z$pBk1=Mc8BoVC*MMkB&uGkLg}<=&VM2q$TD`6>@_9IE2o)N_@XscK^uIW&S-?9E~r98E7sQ-h+ZU2 z8q%pM1E`hgDSjhBjBve>p{<0jyR_9^dImTmP#T#zh5`?bnjvjp_VC>WV zCO>VDTJl4Ws-j@13^iuaM0Rz<#)?)gODp04Z0V^ zk-I@Cc8k$Gwc65<68$>d@;C(0B-x1`DGOC`BS``cHa;ffQ%(j!pziRg(nQW7jK`5C z_CNi=hZ0S2G$Cnj5UY!!gpoANH)%NE}@?K$FJ~4#Cg=^`HLRU;nF{ zwKrAmHnch1x3aBbH!iTqs4X1pC?G8IRE6drp=^*@j-!fO*dS{4NWqV^-2VE;tiMZ7 zK<~`bK;cu9ljr%LqdE%Qh2RZ-fcM!ZcQieFcfSLc`C;cBw%%^*Nswzjv_*4F>*m06 z?@h0?+|q6(zQ-lVp{gUIYdQnVPn`ubKT$2fW9Xop9Xi6M1e=T&daK86_-%G#muDfrAiywXL0lA{}2uI|Oa#uQP6SNzUuqX+VkU_>;~5GF(^>39c8EE=DI|U_k;KRR8;L-P zaIG9fN0ZJlOKKHag~KYHHa|5)%WF$UqC|0|VGJ>5&^8EdOZb@c)q}|)^-d>A3fT~+ z?&JX+<}s20wK~F=8oMSD<bEfN$tFFBb@Um%U;+c~;a#PV{bCt&L(Uyx+SEq4ZqQT|M zMa;2l{^Ui_#0)Q=eBAEv z@?P7ox${2@0UpX}0QAConkdR)OgNWLMW_$~KT2>S%>hqM94~4UQ;V;%A$ZV2wVumkgydsB$mvIc49wUP+Nfh@VRG#%pZF187IEv2=6lc z{1@6!ro6xGdtbI>%YE<5%ga`oL1y6)AnAM&A38^zFK8!S=rX+Qv=V5=AyDd!!>qOw zHVvJ0PA*IhOXTN)BzhV0o#t|FNrK^mGxi^vJS7I3 z7$qS`8Vtd@Hc6>g6o!4&QYEj8X_WrI zSJl>5L&^;SCnX3)Oag{1B@@T>Q@Bf&T72HaK~F54*{0}HFBKcOmAhHE1-Ko!FLW1h z*KkJ=59BDhwpr`wI4+9Ry7IfKvxFycGDDH^V;0qL41_%ypZ$u-6GIcL7=gob9broJ z=K-~2VyKO-3pA73!ql9Rr$kjU)IL)Zs=4cNLN`jD;UC^`o3?sepl;V()WVE9lEk8D zA*i@iF~|%xBQRA7P01^YW6wDAG+*%cEuA&jT9aA>=Uc%i_j10mo0bBSNhmTCSQ@@r zs)+BfrI;{GllV(pUbjtY-IzCeyOQChxU!Y!q4KH)A9LF;XKDXIh*S;b`RTeUs zS-eKEq=d8BkQO`g44X-=N z?MxG+b_5OF`mbC=syr)mvY6^l4XehMk74B7Zolc)+inDtm&$+;C`|H9Rb!H8(m#0q4$Be0bThQ*@e;bY#)Rx;$S%jxRZta0W9P zQ)-SC5|gCCC6PUHok~nX6GPV#8NIVGg9+dV00WXFsbau9o$5~8dK}iqF>`3k+WIYz zLnbH48z4dz8HB_D=%y>l|HIgQK=?i(XI41RlQZSVgSo6u%`CD3+MZ-H%&w_9c?^NOw`LUfcn*Q z*72^)Z`N2_+{GtVgVb)s8lvipUnAGc!lMlpO%A{ItcZvW3=$Mt#&Yp7pl||36E6F- z90hSv@}pp`#A!7UOI_SIyb;68wE(RMBxF{n<4Bf;KEvVEzmf#1S|!H0c%dlO8o#jO za8`@zE;~p5v<^uwWAc@VhlC1}q&oVVZ~Yfg7fygPP2@^L44nFoB1CZ}cDg++$Qc{f zUXJZy3AG>#Sea3>OnTt*t)@yivnTvlTymf_|>Y^hIKPkKFOq+dgsQweQ*fwJUD6!HagEyNdJJ`^{SiUUnXPuN_`B zZ>v=n&Rx?PY#YaRnQbBi%XXH#_{;AvbCNLFfEMuk%cmT+J-mGGvAZ3P%+3qu34A8-_ zAR*jW0FI6y81ziUz=s6kK|G?elSvc?0FVQUfkaUO&u9fa0Gkqcr=#a#Bu zk?O177di%bZS*n)`(RlBSib4{&)Oq)@r$oQg2~K3=olylhe7G`0?2X+mn5$&j(KDD zLVyA^q+3u|nA+;4HK-w=tyG`yygIc70>8r4mMdm;;UtkMNkyn2Kt5LhK(4-0E40=_ zN6~J!jQe2O_Av(?s~;bHM(1E;hp}xmbCbZ8Z?mENo_D_a#G?}_Xr;4}B>KY#zN zH+;f`rmQNbZtqih|My3EK zfNdx&NtB~7he1-rDATE_K@)W(pR3mY_kQy6k0QwAYA$|Z41;qBuPO4=Dxav~G=*^L z5jEJ7FDYpPk`QY~Wuc@A2A$PA%(5#nv6C*Cu|Rst2zw}4`91eNK|BjQ0(vM2qq8|_ zl2eu_eScpEez|40Lwqmu}UGfLmkCUZ+J56oo*EI_Tj2z#1 zyU5yOML+5Yk_~u}R-wY=Gni{7`s>Y1Yn4(p6p-JJRTjA3*DUZd>lzaGnK{%?j&YLx zPa2u#6kqLBM5RRO-&hq;pqZt$b%B&38n0SeZ?3$`3Tv;k z`fE1X0C1kW!`9v{*oF6s{^S#m_AcTVKl{mVer2rvHL>QG)7sW#!^3yYz7wZ2uP(u!)8>LbR>M*Rz^>tZ7;v z5k)R5UiR~#AW9O&)_N!-X%P)J?QxOHlOq1cF4CToYrd!G<6EnKfp*GU0h)PujDq+I_{_wdNYNaF( z{THbM<`hW6h$>~sA+6MkPIzMRkW3~ZuXHY;T`3icGgS#ek%eM`U+b8`6J%~OdS}&< z4+{&tY^ribAlLgY%MLo2Z0UoPv4xkdV`O^6DL_=3D9!-0ESqjdpkqH)TJsue_Q|~N zpsl_(9<1KhWWH{)4ej^}aP7zCcb>Z~?2OavICZ4Fy71MdcF@xjLddUc63!nK44OI#0x3YqtWK@*=qA~zq1IHIl{2D( z2o#o!8vN)<5UpNS2Q$j74nx^V8;+>d9;riql@+C)8HypXqQdE;Xh}8cwD34<2jQw& zO=F9g=4D5r{G_#?2QPa-K zd2B$~CNfa$BsSD6jMAoaN3!{V^U?cmcG4l+p7Hvf;N>@+IR6d%ZMO3kt8Y4Ig_W0| z+}!0+GI6uvW!HRpgxMDc$DGQBlmRqR01dVU358K?`=UUFU?UzkgN1y7Wx%-&(2yWH z9tbf|8A6m4`U4d)fSI5l7l_6qmHc*HWd|O?%%Yejkv5$r@aR)A*L&JoW|P=KhJPVk zuk-<4pN|Q+vkP9v1OJAF`S?8Y}ySSg-33}qu~RbfsbHOL7g_rWrl?CABU zKK>!6ufb$d&Ze`kn89j+$p-XQm-)fv_g;SCDaRdp#Y@aaq@v`ri89VOB zo?MRo;8z&8O{nZBlZa!(m>T9wh$&1V+N?A&@gSO*9J*-p%gl->fR0F1I3)(eoI)!? z6a#zA%Pu^`n$)r*DhP$Zq$!00!kmDpyuv9tltE0wr~+m#w#psZJRy z>4P|Gff5mO{E|3@&gzo?e7;(YA}W_p4q~+y##34RO0OXF8X*PJFb00Ps=jDhQaCwL zBt^tul|^eYNiDPJ(-{j}qY4L4IW^x7HA#GYuuN&$+r! zPg%xe9&Mt~GNN;Z@nhDH@(NVt!KsvrFr17=sW@dp$4qj)hR)%X5mAzGEl`uDWTPbw zDS()6m}hlr?U~3`eeENxS$Z^U5U20p){EcrCMy+#3KqEhic5FjV;6T-L4SU=$GySM z1hU6$oIzE<>HY`oZF%=22)_CCqdym(e~xcY*kix><1CkJDlRjHK7|~8Jv7!j=jnXEzD=HPCG@*?lLIA%Js{BvB=M6@y|T< zBv>O8)Hu{WEGy7MSQlt00a5Kxzq{*(zNuskQ_^T};tSK3nPiP+Bbip_g0t2rcYXQG zu&Vdr4xGRCH5=@@({`@+bePKX7hU(st50EPqBIMn?BnIzZom19x83yZXP*4gkH7c+ ztFBysgSEHZYSX>|qn{Rg4F}W#$gy z)z?_vPe1$poh6rgflJF>^93erH(3l?H@~?=&gH|PH#DT3y+zTUI%we@yLro`gY4St zI3M^s>0z=oJS^?djVsC*Za6iN2D1RMh^>dBC6NI8=tM~b zk|1DkhL|E_!c5*}!M^7yhXh@TZjo8MLfat6)PD1c-0@x6hJ|vKtoWU0J;(pm> zjFYxy6d3YH5ml|}9GgbdaVVl(RhBVLWL(@y#U>Xg`*cCVD5fWY!YZY~J#jepMlK`A zx@`@Q!sf9`4-=XVBPuCFg`WiLtuPZsk}U}mtmbwtvITPRG7#--w*BQ34%+&hV|IPZDGN_HWEhaX+?q=JrV)z=mz@_CkE3Ae*<%Kpz~3Adg<-lj>lR;e|H^>a(+7xC1!p^=y4j?lv1W*8H z=d0}_+i1pi<8{}9!!8~ag_HqWLZsri`QpkiaNDjl1!PhewV*#`I51fiv=0qM(Nk5` zY_GQ!jt9McXQ==+PKURJ!_+8RgIJZVWyq9@Lb7m3xt1YKLvi6{=dnA94JHZ;c}Hl5*`#w3gWaI6=|WUpdm_X%1=|uqLf*kf@XEog-(ab zS2R&pLQax!y;IQ@7ybE?q+*tV6QmJ~4H z`DEg$g41HvjxumlY!!+hk8n>GVv~tNjx0l;MFOht>8UB0zN*g<(hfc!cD~M!4nwJ| zJD;Yqi#W*0$HJ4Pd&c>yx4=p(ui$G|S3uhVvHyr}{}fV?Di^cL28548^nwSpr=SwS zG=&Pg0Sx}N(QLN@vzxIuT7TWwA9k>X)XMpm3(f_N(dpw{u>kYz*rSf{I?L4@QkKG8l`!ST11Y79Cfi_IdaE#SLCLSUl64&N+L1$(A^I;))k-xigP zQHMW!=g@w=&ZWQam!0AC-h58VBOAAZM9G?PqrCm@t-gTD79cAPb= zQnc`TYp)Kc`y_nTm6kU=97p$B#;EWP!KSc1cg8qW(_fz1y z+pxrgS$q0&!R)a0>>z|s5y&jVSzXBb*>bDAb~-MQv9FIWXe0(;mDHEX;_ z5;b#MvY5Dr3cB%wl9h{sj$d>vfOyKKn}j(5Sr7?Kh%6jYh`Mr&z1R>*!-+?jOO8}7 z3Cc|+zEqq_G36BjH4GF}YLtjWXZ%B%^e2R<7^)6|hz(t$0t>N863r5mtXyp>1@T;ZfU3se*4eA*mld!3{HE{-iUh1Ba7;*1ngt8 z-8bB0%atHyco|0a3A6LqoIt-J#Rs45K7-7Dd5Syzh`AS@yx^q6w%cv%HD9yta?j!A z=iOfh=p2NDmpuxhIS|5hfq}3+;lyC_Yc_ zvt?(q1tfKT+o2ch5)d6gmIPCi6U4?tf>fK-aI1=N0a!)D>`M1OK|`qswZUGM63`l2 zNwdJhG*>Sa4M=%m8EuG@5C&Hl8NuN=WeG3iltq%LRBS0AfYT}Kj$Q-KAhZ2t4p;{6 zVPpre-}TOm_$Zn|7%q1D`ii%ocifTtUw+BCoR60QXOP*K%i(4Bec4=gB->A(nP|-^ z_Lb=)YNnWaCI~~zoZ?V8lM(VYza$Cc!OZl^`ZAjY;t3#&B2MGwI0=-IO9E-=a4`jF zg5KODgmuTKhS_cA%80Y+FCZF`91bPR5WT4_6*1){Aw_(xeIm()Q=oTh94x`2ALB9QVF z9z3Z^0y?7Na2Bmb=y)W_J31n17GW0M1ZB)TGFt6>G~}aa-gm&vqonRmTQWyO5w6go zRZ41P0ZvlN*RzcfSrCWjY0Kt znQ>?X6u8^))$2JYy!JY4`+AglY0D3UXH14(wU5=(S3eFj&!4xG9po)uyAkl^_n1~( zZaMz3Z+M-@hmU7nFy)I{HmVw_8Zgdhe?mzE3xmeYS2ze=(M(X3@G|>(rpt9a@ReGB67wjLX6MzCwcO)>>m_Rq%xu&}TG4 z&7icwAROM*tq%7(9-ww!HE2dAt|yU>R-zqYs8$gqi?X$A*6@w9SLNn~JF%a5@?qCs|MlPe z$X(f=x#kncpYVpAcHRzx{_XF61w=#2c9!iyL(?t<=ebf<$7!SsP#sM^Jvg>H=5Ox;UEKUx*Xz3;Vr1)y9ujXZfANY11`tc_m zEekB8F#(qMU-ceMr}v${=-M}T8sB)+b%=9!*iP@*+5<*>pQW>GG4uCXu!kWL%F|5- zzd6g-o}$`TgKPXcvG!*?XZnijRz5aa1o#cDO!0^)+1qmIJH(ux1*Q~K#+J~0M3Tch zWf3MbO5i05$>=2wih}5D*r=DJB=FM>m?8mP-m;4#%2jo+(U5KyaB>JCSs)3RA=xA& za%m+OCkX>eRgPFGvSUUW+F~m&21yhUr3nU6lXsLNFNXo=(4Qz2u2SMj4oZa*B}^rv zl8i1qIY=;b61&Z1L&z{Q@Qk`oefne0V_Wosz0fosVb03KVTPtn=g_oQUNV}+=C#o% zqtPgrqM>qaNJ9$+*z3fIBPdQ;zjB2Yj6I{#>o06-YA(~<3hYxFXCA!R^a-=~Z=M%^ z7SOi?Ru!Du2d4ukbqz}OQb&WZaj4LmsBYy;)uE*-MO4P5^-YwJunJ(5<&bn>E#}E(81Wyh>hk@_uoPi{-#hiJAI4qwY8HiFjS(HK)9zHiy zI--JDgA>>9M)^G{0d#no->K|=TWY{$7#SRP1{*;3!7_mCrZ3pp!E90c%iy!{H}1T} z71{QeANj^D&pz?xNAJ7tCx3PS2d_NU_sa(@+~C0ZbNrsv(Fbht`h7NaKekVpfn^7< zkKAh$yUg=8e+B0%Y%m#Q2Bm$}?6NPAxm{)lvrjr?Yv-}U%Nwur@|Bib(j4??n7G-_ zWBYR16m-5C;semYz;|01`rTVLAp9U+0pPbNH3T^NS4Xb}?9O|`pmtns zB1zpfnpE0Wi}+=Ph;gb2u`*&%Yn&XSBJis|POj>s+cgH0`&3yNOm?-`CqMQ;Z<{KN0Dzx<}NPq_HarvuLJ`*PVA9PNJW>p%11uYT!zc$x3_ zmz~ERh|lzCnwT%lDrTXirmBGbUq+W`5T+R<3!+&jTHeL|Vrh8i@)kS3Bwbvy`!vC*NvixW=)sXYZ0l(f-ecxs0vN5|u#=xnw-v;~|7)HQ52oY)u!l+p*)bo@K~Ngk|Uk?pY)qV1|odvh)&O%sSGnuCTKQjc0^q z1nkpXy~fc6^4e5&dl<|JDu#twq0CW+C=`q$tnM&W0GVkji?XnxXXUbcrP2_7_vNqc zMf=N?1cexu%rN6H>;s1A3uCfoQDv_zbd3Nj1&VZloO&T;4^y8e{licG)<`gO$Ezn} zQrP?@N_J>VcK4*=cWgjcdOX$g+Ezi>7*XCo&`>q0I`F=L*3%9SlQX&XuK{q`J1^12 zju4RMT?YwFWqm|>Mt-Bn%Md%uN>}s%T$bz6Sz?n*5+`FzB1%Nr$C%8bjxJ*$LI9n>WM)X?K^Mk@E=;U) z6%m~>^i+#skXPa&zPu!;7m&hF6rRMAPg?a#^VmwA&Z{hJ(c_m9gQ|(v4z++VA&Mxw zRGdMige!>7m~mng< zEI1CRn1-gLL1hpcQIcAJilSI^5Fu#3>1@zCBqL)>khO@wFu0BYX6dFuyrrS)Nyi;+ z;2DSBgE`?~-M;n4>;1r)728JvCbC+yQLQ2Fk#KOxVQkK-?xc{VtiGl^fqghoOKK3r z+Ez_92sx@hh*g}AC~4#ppjE9Dv3AR+IajO7r4+(7d;BOvR0YLJw>l*?;u%FeUtqdi z8AWN^*ibHkh6f61Qc<|%Bhrdb2SoDG@l&IsVVw#y|)=N;In#o3D~>AGe$ALTrYE z)7S8_kC!L6VbA=e6ds$)!ajDkgA5~&-(Lohh5zcikB{ToD0oESWjoQ2+;hi6U%%b& zFWX;!=B_2+N9I+)ybUJx-{ z3@n4mP%|9e?=RbKZV%d#?9-3f@$92_dFyF=A2e^y_M5CUXZ05^{el;`z;o#r`@C&- zyzHI77ye)!bOr?jZk~ps-uVM`a34$u&_Rn*`OutHfxxUNHn0v`ur6HO<}aC|gvbj3 zhH&LYoPmCmv&VQa^9kgm7p=H}XUwv5&?`}G^5XHT4Z;O@Ei#^QwuWI|fY%BG6#I_P zjW?@+nrM`Whkj=-*eGiMYI*C5j0VI z>)L@!?cNojYa;wPI-~RPlwGPosbr!{RnTyXrp9yCmozzR%?ZHEj$zwkhLJm=4LG|) zdmP(#*_W@CZ7ZL1`q3Ajdy@OU-gm`YAZYX(uK6=ZvK_;I^5L(#>> zFc%mLNJ#-4icLGj;iy`r5|~d^C5kR0uyqCl#ac8-z%vA3V(u6cgvI`?B zg`S*1nvdscPmP`n9{K_v3gfZWlgqO}u4EFM4oX+9Ww|${1LvlyqGL;W%$x^=s9Z#) zDxUPLB2w|hhEfIj6w~+pK14b26`+HHT$GGxXM{a~m@g zNYyAz$!bA!0d;iZ-Ma|&Wp(564ZHreJ?hnp@5;?1UDgW?GL@*mhEt2-nwX7qb)ly) zRW-h)Ub7p+<}ZM3wENkt$tTHPKHPJcl}GLPQf2g>kl(EIy1>A8JE+07%uAMX^!jiA z?yvlM)MJl7_@`H2wev39uQzAS`|khx6Hh)2H9L}h$=lzu!;af*z0GE4pL03@u4n_m z#G#4e$X7I#fN@a^Ech%{(RRS$6rTV%m93Su3Gk~OW^OxQwM*xG1#sd?Cjivm&-gWP z0Z^p@APhdg@vPHiF?|eCzer^Od)0&aJ@383&2`3uT9|rfjesb?;^h-_0$R~QTGHE< zTG)6nPV}rc*99hWX07fvSaZm_RTtX@($%)Fk zP9hf`})+w71XXN6+ZT%y$|Vv>ip@}@uv<2R8tgQfKiv2(T`dgpCxUu&C`+xs89}m~(7;azmZqo{8h^k;!F#XkT zRHVkwM~|oa;z7|SH9nr&C+0e6G*n55C>R7#hKopGbOLL9QOxMZGgse9N=~^dZV?|t z4$rhJZ!-B5P;rvgOIT`hDjjji86_qy24T9%B7kCq!TXIa$cZFQDk2 z8VxgjTFb=A7sW>4w4Nx$QXT`ze9}-VE24~&%A1~|h}Ox>4{K&FdxOtKVRBeMa~`jr zRoxb*wGISih9cTN? zlbgPtv3dQJZDkwEuD=G8?JpO#k30@(52S31*`~AKlj7Wu{nW!>y7$h{_?9%YN!PcXyFF8gvS`*WZEQ@7MU54_A=VMnoH7)zXKBFvOEJGfzSnS&+a zG1tr>0U2$_2w)o%ZPox$=rSgU{Mb-9lQD}l1)7^>!DfOt-;)r3Sy~y;hpOoGBwtdF z%*3K|)d+tsHIgud2&AfvL?H9VS(q~^5{o&>#Of3e7w=;QC_r*8BY;g);6zUWL=qlv+4Q6vqD@k; z+AfA|8q9u@o*Y+Zag2n>xzG)GDBr!-F5}XtJ~v)r#bw>-k!#qTfsJef4FK6L2jTWsQ}1K|A8Kfl^sfJ%%>aK_kxXdJqB z>z4t5A6Q;;sTbH&HWIwv@$ECZLF;4x9k$)dJ1Ykdyk+u9f;SSTp*iB_J$re-mWrq11XK{S03Q4W2v3K}{F-Wf#B`tr&Rjavfg{#Z69hK(pk0H+ z&DcILvCdd|EKOFM_MqEM_H}?agoqX^3lug>pE>Jgmva$bcD0%rnJrn0fRU+f08qw} zY##!61Lht=!_SLN&Z`?Y=NN(Zk$tWF_P1VWXW1HZK*t|`3JW?%cK&yd(_R*Ls zRgDx>Wwne3iCC(dN{WtxlPFF=l0dx^j|Sv0i(*JZSrDh4Trmp_{w5B^5<+-#YVgH< zsf)Own?ku#BPxXi#ij6cxDux|k*X1*kTd$7K+;S%(keng_U0h9rB-eQ73C8suL>Gw zG3x?E%u)%7f0S6`q(AwbFgC8F{3v`vFr<<|GJDE+T(T>&6^H3!Q7K%KRL(j-7JRc! z5(;PSZL?U|V#^7XC5uH=Srr$LWm$BTqO6%PlWg*{%SLpNd0g|=&n~yc3`B>ZVPr`8 zhtE91^{Fz7!1%7YZRPI!dg6hvJbvF-oX7s|Q{Q;=vrFZ^;!Eh^p2C_kjP#df#94=CcJDRath7{3cL&{!^_aIZDl}sU}V`LWjwZZu%XND=?OK1)pDgU zHM~8r=dP7fn|cCLYa9%!Nl6R=!1)LIk4%aTwusQ`WrPHggtMVBil z3>tGVa=h-r_FcDM_tYa_b4xbUC=1FAVn?wFO*hUYVsVLb(X(%}6-^~PC@deT=2!H^ zeQYEUF3ZT>EzsrSWZ~p3d(8P# z34vZm?c^d;ebJNLB4YuO1;1)yz%NQ*WnmT&jfhR4LTeVHs7(}?C=`pBm8~cVoQ#B| zl|YWDoGhGF{-{)C5t6HMgz+S*T#NX!<4H*V$SxOVQKFKTs2(Do5uPC9St-i4_cZZD z=y~CB&~rqCP&=`ePMRuLmvo~%S2ZPJlUkwZBt$`slP`=&EI1~MfM^o(QSj6wNvqj= zJEY@sx)`Qo)ei-dT+7gbolpLB6JelIg(^$Wa{pepX;^5we*rN zV5r;J(sZuavTWOHVB0gg{OG4O98k5f>p-bpW>F>?6E3UFJ{rd))4Wd6I9$r^aw^-m z$-dBh)oMQ9U4cvxn8Bk^#u(=jY=`34zxWwwAfw%97ay@yy?YeZS@gt4Pf;JxU3kF- zev)bXxm(X!f6n{<F^3p4P@WMzUfMBAMaP~WW9c60cmjMZ#c`~hm50Z+mDdikcJ^3V?xioC9J60_)&2Kg;9UeG8G{`(<6poAv8qv(!d02C`YNTz^*cHX z{HOo$U;q4LAAaGBUg%JP)51r+?oh`y!36K1g3J>=XRgI2lT#xFsU0^)W~04D8xW?y zq<&kO{I z8>DPu_d3EMZKZn-dZVIe^{+d=fL}X$UJbb-{jontT=G&q6frCzv%W7{|vEZiNAcIWmv=bot|KJ)*d|Mol2dhmmd$Df3u-XZ})(gbtat7!t0IW1=q%E*PEi>(xFfBdpQzO!t>k^bQK1nlYI;ryaJO)P{t>td9Iy9pTBR z1_{IURz&HkiluZ zaHglgbo-QjR6!9KQlLh|98b~wDEvj#7M&zLx7lG8op#f7mJIV_IIFs4-}=v1vB2WA zD~Ll^~JmSJnLsfO7zU^3KfmpSYVI77$bWn0W3vna^S^|f+% zxl`G;n4Q7yyJjb`JB$rJ_eJw#-?$TG{`O;EhnMfW>)OX3xZ$Y>uD|n^tG3$s#qjcE zb9wi5;pO?;t_m!J%wY1uZCA6Q3=sp!oG83306TX)+qcXBwJX1lKX5B}`Jz+zI$+mz z=We>P@0VRbZ+>}ROx$eWFT0}>>V(eRj1AcVp`p0}*(3qWFrZX`4se6}05@z9V&UMh zCg{)a*!h41hDdY(qzY1bLm!7O-}qcVJt&N>Q!`QM+)l z8o|zC+nNO%9<3p&t#INI#iK#wLJ`HLvXW3#L>8%#xTIR%d3aInQ}zngZW!>(8&X~z zHM=O=hBCYiCJS`mm#CxHfAO)a{EXCjXC4bLzx^#|zU$KS?Ku0c`7^5*M4 z_LbW{cjp&B`@}|nnWLda>_WuMav?WC^=+ScTq$SBMZBSh^ND}O$ZvW~Y$G$+5L>e3@1H7ZmKlTgu(96F?%jB?3Blt5|^9@*uRHw7?M5e(?E z7-Rf+>NVjsBwsGW23;wLa;13%iH`ol!;Ml{!XC=3M2%o4S6jK})G=(=Z8o(iTC0f7&Q_Y4Nj9{A#^b>aVQ=4cuRY3!o!v31CJ6*Nv#uy*FafGyGcF9F= zw#2!%i?ylE*;?#RP>cQfxA6@3`!(QF7{WhC#y~QRLVVY}2<3V>D3{6`; zqDHeFPpqFP(w+|YDpB(~v!ve%2c?xoau!-(nc9@E3M#Amly??XQ=t33V4L-x;C58u zNI_*N>5x~{#a+cps;ishQg3C&0%B%Samp~w^f%9 zID786zM(Z|<>+esm9*RGY{y`}*d${07#2uD|xvQS@ASl@*-LMhBKb zY4lHg@*^joa@^%tyj>HuiOgqoIofUMGag*0cHn*sZQr|1)C1IUH9s_V#;GTO9`@Nm z5(B_CpLYmAilL5fPZ>bAU#vu5BKy59=NU{5L$R+FU?2Devrn7%Trkhc?Kd2Kgpb)T zeaG9HGB&T@eBRlRvXilf`em2Ab>7Z9_}JeYCrt{}@O75qp)uLUMve)~v3pS@W$~I9 zS-Feby?VSX7)q6k6D~`16lEE8jBpg)$Kg>lH-t1e36xZpWTHrldH92Q1W2y*#r>g7 z+7hB_8UHCjNJ;tAGUZJHL|F(ocev02A_jw>oS^{zifHzMdzx1&B2KQT+Tr9kn1#^^ ztkxAL3%b0LlwMNGBTB-CCnp?prBqNEt873RwzYJaiKd~E#s_{Ka^Q?W>yK1RF4t0n zzbuk47_r_QVkV(^Jr*S@Sx8gH+e`K8GYwsDjrALUj-&XX9xk?M_>RMZeBi!&`GIC1 z)Bx08>3CSUPOwTwCd~S>% z;aV7HN%^DMYg;nYP)6gNtE|QSdI3L`WFa<}VhO2GoN9qh6i*$R8s){Dgj7oBN>po3 zxa=y}SS5s5oVtUb9Lh|WPG;3Xj2WkZsH9OeO^TKzN~VfqkUgDcL5~x2E=t6h|Bu#q z&WLScAAU=BHjITmONnodwVqqyae8IVWf8>_@(o94u_OnxM+RRMCnucQgANP8+8N=X zF_=8A{0cGy(J=C~gPnn8r?O#Wka;+p%YCpcFkJ8%WVXxvmr(X&?$EyH=|}GH`^%5r zfBoIJf8?VdID3aJmV=k~o41bpvG?0$9Y?aA$KHMGRqYys#!#~jW!HH*bnS#TbnGy; zeP?^kzGe3PvW;e`;pNNDIp~-JwwS;Dy702^m;YnD?3PO>i3i@`2^>GW_c`J_iwZ`kG}35d*)FGS~|mN8#H$_9X~5bX&=AF`~S7mid8b zd(+HCY?H&-ADqBj<#Ni^ETZZ{RgZN|q)pt0Eg*>#4KJ&yN?}9MCi1cYv;ZD-O^VL1 zWn>rjhCwy8IZjopiAazXCkizdIfmUXv(06nFuUE$$IEt>JFwl?&7d*J-2U1%h~;1gy)Ddkl5-FMyy)0tn*I~El4#xyZm%qc`k zTvG|1L4`*E5hp*R%ls0U)lJVw{7g6{GMI%Icg;|8DA0sOQ94cdLQxbZ zQSD>-9rYc(R%%usGa%P9!oz6nFMB9>7I?(;kdi_It!hsVg+kjD5I`Tw*VmZ26s~kS zD?KGKSI636x;p3yl#3_~qC>bShQ_~C4bDfQlKKugD6-d~z?3kB(;3fb?Li?VA__V1 zl!u_!U=IUU&f~rDw398-cD2CU4cA-Gy5uahW1`D0vy4q=yP`MWas#m7%S}ibG_XO& z|I@2KfN1m7P8nSA;XnJJliTP3vXj`HfP=eU7>^4d<-o=XCvAkWzdEWdFoZ8QM~`naCOKn$$YL;Dm7qVWSEJ zc$usW&fp}Z6Ka-9z7kojh#??WE3~d<;81%*I7MBSwd7JSaDv?nhuwCXt2#hsNK+#) zUyVuQHX~nZQ9Y%P^sn7dczKx@y$I@e0{Mhvk3`X}_3RiS=7lcq%m713Yb(oZ5I1z$ zByw9CM5Ya)S_K7i%>oTojk2qSwXM1f*kV?3)%01egYL_|Y+3zlkg%m$P|(~Y`^;9d zS*B;xKefXb=u~Q6sE7b14O-_VG_~0cEQ@x6BFoB_E+UGHxqNT_x#`giUDLxGx~dG;w+e*Ns{ zKOx+aZ2R^7AeZQ22k*Dl=C9duvyB~hcXWTZowtXYohVc2J_~o>Yw!6^`+IeuAB}c> z?ehc>SV!Ay=HB)0%T`@&RV6xH{rV#hv&-z%x-%GlQpylDLJT3yGCHaa4H>bf3;^z> zgcm*rwzkq=?v3X%n7!*T23XV@Q-H%`))*w55u)W$gk`sy3F}!=4KB=$bq@na!dUbY z61^k>5#>@G>l$a$8jA7ob$U*CNK3tZ^c=P-QZBU{QSxVXvS1$4gtPN0Mj7>0A|9eN zi>4U-=$wqBECHIQd_qz{mT6{LM9n_@rUng@6Eh#3OLNkM;Xwki(I=GbXjvyL@^kyIAsh?OBKLi z96CF#$`+qAF-}ZdFOw-lb7m(SmGf@XU~qEMOH?cQ{M~kT3c`2I&LACn;C^!H0&2@u zl^cO%)>%Z=n4|htGk%rDi2+-3VoRWKy~(d_u{9tY48o%na!8Z0>X2EvgKo4abd;#q z2%&!szPL{mA{~Z6rRPRo5~h2#KNuT$ zJWq30b!?sjQp3fTF@To@4ku!J&FD$sm&%WVO+Z~yP-7^eKwaLxc&@mry4l>(l z7PYP1Mss-Ccg^s!4drh4f{TSYd&&bryA2F*MsYA3UjEJ#_dof-SN)!p@0Xu=;FfPb zddodu`smf~J7edqSA>`M*>x?>k1roGe?#|u*yX!`Ne>uGTAN>BZ4NWl3?|p>zAUk*vj`Oq%34(0l zKBz4y9R>7*cH1_E24KTupWoVffDE#1t3D{IJcQEASnx+fS>d28xcC(LU zc_9>q3YgR!qEh6=&deR#n^^2KmLzG@ z{KAz^Tf!|M#FA4%X`VkaCcom!K9UfX3n{xQ)@`(W#}se6;aYdb%B6j*T`gatNx+X@ zRG~`eOR7OqRVq5RI16Kw8XNw>ndW@CI5px%?p{LCiHd7U z+lboM(r3FF>fB_bISy(2*`S?w*ygm8k8`xiXDMD8*=}Z7vR7G{stoLajRH(iqKXdS z9BO9S%7U0_Dk?9-H4_(|K1UvPH~?j9-fs`tJ=DB4zBUOpi*lk`KtN3!qK2-{WgCEX z^z@^^ei4|?W~*-1(MCuEPv;645_*=|=6t)yik)RW>u|3fOy4q-uOl)wu^Io~KhTi2 zTpQ)0Y0kI%sg7EknGO5veU0oDih*Hg%C;0Gn^PpWBbiOu%&Jlpqhv#rPf077C61~# zmmRcLHE%7fz^o=%*t5n90ie}xuMf36zW`g;b72Fb@%W8wXQz#1Q7r>0-};5m-*)@W zcYOH^Hk4sx_hZxZkVE%J`Tk!5s!#c-8E}T0ufO3MThdfAB(A&mQ;@hHsB-+k4Jw|V zE*|elwgd2rpyB!FoM8ufw|P7Hl_0okTXIQrv-fHVp(>k$WRoZ zVFS!J7ovU~+{erC@~%5>Z&h~y>7qBC=UrJ%q{;;1=Typ&s||&7S|C?7#s^d6&R(Ol9nS0YZ0h7&(Z%WPRlu7yjApD&7` zZYo+W(36iyensF!BtMyWk|v4>tFjDPKAt`~qbakkfUsv%V3{wRiIOQI3yR(0>`77l z;orncP1+*zM-dpZP6fzp6L}G&3@p0}8+L9_nLqZI;bLHU@?rA*cY?zMG7r4mjx!#q zj$}KJ?SAYh9{BPT58n3p{Wm}J#24LgV>5faTAw^eW;>7l z=JU_;O!P(<0)^Uy0X+||!k}CbFMJ2G!F0@?K&vFoe0lp0S4LT2Msy(8F0$aOdZHxJiTd%RX3@Ly6iF^G7vvYcE zDigtc$-?I|dKq1uaP}e7tGUL|;@DtWL5Nw%ETUPRj2UlIo5+a`77;FrP6$!d7-9U1 zU_`|uAS$&|92;pgROnEHdJ0NyLWKFE zgmVI;p{j4q7>^1nu@=KTk_Z{DrIl732~|gG(xk>9noLPbB#Lk@nTQ#RJ-&Nhw9d03 zJ#alHxGY0a)1Z4m1YR+$n#aR1l;_*ZZMgB!+z)thg+sRP?CON7FFQkUiS#anK<_@aw^ zTW#MNtoEZPz%pro8)bkI`2BGs717`fKaIoZVC04+3Vfu4^p1wgk};9> zpnkNw3NJ(YzHtTiRmX%t03jcG`6)iQ8Hqvj&TwwfDTP3RxT8DY}JB5E>6Xv8fj?l``^?uLcCf1&1uWNx9(bUaY&2H|?ITdT3FtoKQ^o5e z4BAjrVB>^QB-J4JtGHakh@3!SY-On#`Q&JS%GY;g=ZliidyItS6JC;XD_XM?n?O7b zhQ<9PW56u6Dky#AB^ggeD?+8D;-m&AAX-g@MdMiJKvsdiG8QX}x?t(vk6peRu| z;i=8%6s^9BZpx5D$&xTf#Dg*vz%ZlF8}M+f(k!ukksv zpWs9EQ&aumR2$8raIu5dkAB19YAD!;$@g>&+qQB$%WXH~@pH@*hDY!HreCLe@?Ph$ zAN=}tzF&Uyo@;Kt_Hy@qO@4xT&-MF$dG8(9gqIJQzYz%BG3<7hfnxC4McGI1yO~R~ zeZLGWgUk+Q+hT^GPd{SrTTWSc_=1g`$KGn=RhC=k#eULa0+yc#FN3R4-kZ*QBcKQU zxy{2SsQ@|<$4L!D1AQFS2G+G(3h{Ax;5)c4P8p%ya349sZNEZ)AR&rpG=B#A{qa^S zC6XE5Bp;@p*+`x2R?AMHL*q*9kTW)gV(@SWF+gAd8DxfbIe`vfvo?WcN3z+X&~cF2 zDg+lJdg1T=e+RJnpfO+GwwqyXsW|OND~_WHB`FZdRk{k&leTINGD{VhX+uERhsIt7 z_~qq`-+lR8{koH5(unVWuk+iN3OGC6WnADbIqFEhW)vo0eSI7qF*H5!wGsg<3?XYg z804h@qJV}4u4z@rVY8JCGCPCqF!uRxJkI^tZp?PyS3kaNfBEZQ{=DCl>L-|=eQE*{ z!)O_SObIYVIN;Dc6-JE0L}Vypi!-_@Mh?39*wky%nQ=)|1fz)0Mlz#KQi0)QT*OI| zQ4uD7&V;1^itJpeO3At#YK+qHNR^8YoPcO~DM@Bk$QKPMS2YE3tt}pKR$c2YQ-p7o zv$$n+p(Ex{ARzZ9F+2Sjjm;F>XT6J17h;9=l(_&k>r?$M%#q2B<()LosSGnqXv?Wo zbUunCR)^;HAm9W<5v{IPZ#ZHLz9QlwMnJ|~56y~74%ae|OgN^23z7x5dZoIyiw~{P1Fq)Nt8fj&2n$Dcb zXo7Q-X**-J2025}0V4PCKvGZ=`_|7x+g$dcE6nA>XVU1vciR=M2(c85i7z_lJ2v%F z1r>o1Z@uX{pD?TPc3W>|m)R2cZ@>FzzRxIRS8*~a%o_2rwq{vl)mN;z+)M19tBy7y zB%KvE)+IVqo=pzc3A3(+8QZRdwu_451qJsH|M=g0Gfsa3wY;T6`%qqkYf=CMuGYcfV@ z-jS0i(q!kX8!Tj!Wv_^=Ye{7C2{FYIl@M7nag&xnb5Lr`){}Hr9eF80!-**0>CAka z-`1qz?M0AxVzwy-8ZF)_IZ5tFIp%@yFrXVh5S(1Hi}n%;1^UAvmn;QLknXBNk&0PB zuHi%xm6#5AxCA0rI*Jm@9zztwL9Zf3D?Qy%DE9sDfB$>m``&lI``z!%{0WF6{_uxC z{Lzno#20?%nPYH=lb`Vc6*U#)idNSq(*s zIeP6arT3Y}HtbA}@Z4>-gpQrQcA#kP4t|SmbKktU=buVm8Bw5qQR{kbbbSxCMXIch z6fg*-)((6{i%OzQbw_E?q8p`%QiTo6MtovWWj?Uo9w32m`zD5SEcZ)5hD4O7+Y09SE%ktth&U5c@c%R8w!DZ{N`z$ zSYEk^lFAn*b2M@-oF?SQ7AH~4B|#PNONtr2?AV4rDx%hW6#O#cuh#@*k)$J{$cU0A zN$?2kiC}f*JPt%77O<*$JXwdm#O8!^J)z?C*us#r@=Nk`@D#961HvG)t>kX{;`c4{ z1R8(GSA-zEx2%3Q$Mv4y@cr^LPkim0kACIJhwgal;V(b^$eo<_t?uE!Q7R1-Euh)*r&=4U=L?=88n8P!_gQ*%0M&(ef)u2oOj&36A#^X-ZpEwAA5zD zPPQAC^#43~+5Rbzb?Lm40m z7^O|;_L13>cA1^BcIFTRIP4QtkD+oxp9jRq%B>_E! zQ-Bao6u)M{Nv0@HKeFt;FQ>BKa^A^)g88EJPqDw;k1|8euKcpU?91hc?!Db*U*G@s zWFwVPlwD^+nH0IqttJslNoG@=V^+#*N}5M3E&=qiWFnf0_&E$SbM24kuBApHcG@*1 zOX5!_kxYI?G(pvi;P>>~%oR5HZ~1f#$o z6jR9;tgw=$M$uLblv{7Q>EHtnAlx3V1G-KtNs^I3d2L3=CObz@seIv(GC$zVp3Hsz z#?QG{+P03bsy+LB2JSK_0YI!J4#1={`9x8G!c=Wd;E&bAZviU^8`v)3i1KPHuLJ|a z!T_d9x%B(@|MJV<{OV@{zH9cN^x3c9T7o?_5zI(`d!|ZJE;-sc;eV`)H~BL#hh^So zmJMZ_=}sNP`>f(Dc9y^2;9kYo%P(I>(QZy-+fW+}Pi?~WS7G#O#I{k2X0!}u=b+=% zM=&z2EKPdcHZJt)A}?luCoCoPpf)Ga3k?i4nk@R16IGprlOXCGwe_tBhg=4Xkpn>! zyKw%lj$0qF?}8&=cd&OOet_F^T=u{Jn;*mA)N8y|9c5``*x3j%CN-4(dh^Jz-*4}| zU7qF*k9XRPLnEW%ZH*Oft{MC$qq);OGDn(I=2~FcfNy@qAR2#k8JqRy7b}!AJxMmr zO&?Y(dPPWL;x_lCX3&~(n47uD5kdB5mfuOh1YZ!oI4QW1z3c*2_ zM!3OXNC>0Y289Zxa-q>MW5|~ubd5n6mXs^JDFAG8QDCl|=x|Yz87PxiAj#z|I#MxI zD1A~1bL9$faq>Bw6XDPE2Q2@1=I;kTuoeA1NSQAT3#<4v(jNtlBBu4x;G9MdC=I{< zHIVcj7u-N3q$+4PjjtnsW&6dVS{^_)H*F$^pv_RP#0^mhHwW+w#o_2Rbo|ug_X$ACPG$4K0= z($bG|KJ?@7KJ{&2|9k1B$F9rwvXHfF@)Q$F%-;tY!eygwZDwPH_IqCs`2E;5pK*FKx4p} zG(OJ2vJLByEQP5}0X0%byYTI~qti`TYM9!FvVCM&*y(64;0!zW`^ez4P3QN%`x1~D zShmru?lP*2s?~-{WGzs{HFK<59h1OE{NgP)=q$t7303`TAn9Vp>R2ZQTK9%Oy>b%X$}Dw+Tb4y5lF(x% zhL%&rmgVG2veH%^lu|9rGWL{I*@Vb7QZdNFSL-;{y*KSe%Yvb@_>||;je3>Er6`-pKV|m;zFSq@c|4YgARR76zzbXj5u`|mOS^RGq9{RwMzMe$3$exC z9=oWhM2Zxt0#XD7L20&FqQ*%kF)TbD#C>yY|{^ zueJ6*d+)P^`Jo9XNehHDK6D$QI7&L#mIHf_OVulG9Vg38UQPPL&7D{3J4d@by<~C=8-{`RbdJlOXAUc`dJmjMqM^FU#(JoPEMhF1O^#e zvP`j{w>`Ga5lK@Uh`@L%)yIL~`ldfLZcPC*!XP&(n#dI5HNYG}hP`pq{G~jm6@5e> zH5r?Ltr(^xp-I%FZc37r%^==V5GXLk5MGrgW;%HGS>ua+DL$R%a+5k`3WW55WkcGf z@0|j?A#Dr)A6xmWnMb_W^)x61xBF- zR=0OSU9~|jnG0tma|>dS+gIt!plA9V2uTV4KvqN>WUjS z4X4HrFMe+)p=Kp_(T!L?#oYh%zY&8&9G_?RL2Q5Ez#540SEW1~v)0wk;4*vK5 z`5&4u27z_OHLxC*;*FD4+S@?YF7kSS1mT4wPMt71Bzb)$Hl=tKHN;?6>X+DN4Y`0x zsF7f%Mr2WTrtu^xO@|8>M8cX#IGXCL!dInuI2DqLQbuas&@Y>7I%QR_A279=T&Y^J zm^6oS?(&jl3i(o0pHxNZ@Wn-gicP|4c)4RCsxCgW47Na#WM9vaXeotUC>rq);ztg5@1 zd~Y#JVP_IJ8uP>N-1hx%-U1pU&=_^%EX6AVnUes^4xPVt>lJt1c9rwxZ{Pi;J8ru8 z?yp?_Ctttnoeqf6agA{bkQbfoYzVvj2gC`xCgQB%L*r zXuN<$qP_Ki00e{A9E`SWxHf76(tB@v%WJ`5XUZ_Lr?SJ#;4{4Jh#8drqc@#N!F$KR zGBuX<1CXnkc*LD^WDV!bRnSbOq2_QgH0?l#1R^G}g%c!UMpS1?kscEpXzY7mZ#nCw zFMRIPPdoKQ4`%y|%#gApW?H&nPTRP zeGcCKN~z6~X+@zTrG!fDh>*RUlqNMrbTIL%P#ar2Mrhi;GET@y2FFxYJqsBrFq&q! zRIM!Y#nf<=uvC>n3*{WKPd+STWD92KSIV7Vk8NyPtgL3p0%d8M9VOp>tDB%wi5*BrJ797tb`3YPF<~XfZaZc7QYoP3^G1iuV?OLQJ6!FJh=& z7)AJEkktt_jeK-69+BszT1qo6JBS)DtWAT#t2=6Cr(UXvDQ(H)v?3isUEy{wFqYe~ zKDJ3~)K$yQ&6bQNwW?5qxLNaedu_uU zQ926owWwgY!PykBls)Si#~X@rZ7112WG0^2(21D%XyXU%3rQ_=)6m|GA`J&d)2 zP^pTD1n6eJHN=`gpFu(p%sm%xE^r4QDK#k^Kzr8WN!xDgtkYV`muI|rr`b$w8uB7b zpAq}m$R0AXX+$>FwzUp`{B@zFTRyJ8SPg_Ss5R%<38FSlG6^cH_3? zV3D0L9%Iw)k?e#Zl2@30^OK@9P$FKog4OHp}3PLyQPI&d0lVw=Aj{5Ddf6kibaKSYpkIeSlh#pG~;wGU(A`Fa+ z1#YvZ7JY4Zxl{IVzR}VTBapIo=_uh*>lYzM$-lk-7f#}QF4(8=EPW@XQg2Fa>uHZ|Aq%|{9(6`3Z1Alc?VCSDBm8R19~X4sLXlf(pI!DP%Y z#tb!QS$6S?VT1%J8@*7l(d$Eh27|F7HOP$j3=C@NSSi>F-(EIg~Ut4)rh6Q?9>fzewZcfqmr3d=>w$&AVW3!+ z&_7TbFD={oavh>BgY0w4K071qigGst(UV_3@h+Lg88s?Kwo5q^t~_B^SG2j%O!L%l zS+J_#vU)?=RqztMo4$){u(_|9Sze_7_Rs&#^6M=Ff5pssw8yaR zMg1*v8;a@?1-F>qUmoB4a+(~GL&~1Vmcq_XmXU|I!C|M#fG~27 zEOmSg5s&j_IGV6yWsn(Oo^>P5nle<&6j@T_Ah+s9WUE!#|;kLW3z+zdaOsX z;pId2-1yMFHacjx*}KX9dep$n&XmFAzL+hAos|O1Fg3aKIY;m4eE9{>*z3@}m-*h; zhHF3E?3+D|{UFa{L&`Sl4oqP{z|H+#3eqBbr+}(EK1gUwFSV_L>A*1rC2DQmE|<7QacgVxTM9Vo-mvQCpbP^P1zQXpCiI7_Lis0d#6xmr!*NRtJr6=7IS z5UPIDUP8fZW)H5z#Rx9$No>zuBe+-!FMF36pvHgpoDVyFe#;wQ&3LWWM%MHUi3Xyu zxzc_&AklWcq0>l&#G`(&d2_K8;f0jJthrdAaesgL#eZ|FMFG|$4X5f(~1INQ;Coy1wuu# zRg@}47-kU#l)haY!jmXUt!du&vI(v(M<`R6w(GKZbunglrNc#zqzpE}*@=ck_4LJ1 z81zR&l#VKxFvz$$wq!}+@2ZdTE z+vBw)S~J;-@NPa8VW3hC33~YXU}ADHkJ#*)3q@pJu3*w2&~03XDWuS?NOK z3@So}B*jysT&qVBW=JRqN!`(8r9#!BejZPOzI_G$!6=)x+MSViMiwdyrr_%VTQb*z zrmQe{sVD0$)oTUUY$~M;N`t$$bE>H5``i8~v+u?TfW8q6hJae0AoA9*+r;la{C6B* zIu$iaK^0TLbTE-Y8`uYMb|^^*Mj^x4-PX)igom#|EtAEZ1&Gu#=4J78@Rec!F~1ar zIw2-EZWb7nQU=K_apD0&o#dJ+j5L2S%~NySR5G*pvUO}T-@d(LIH1G+7Vj@FyYczb z7reZDzrB2p%*iGr!efmE4a7JiZ^~QlgrV&ssYn3*oK8WJw$N83atpG@jQa?fm%EX! zd-lms_oV`FNqg)EJOo0u*AvH)T2`>Xg0TWp}VQ(?RM}(l$umA2B zHpzb9_Pn}_b==X1`|7wuSRpZ3&j=E=21*HqiYew9{Ji;k*AaQFMMVl8?Pkpb_HHYh z?=pF209LgY!PM?Y{?MfN)>j;`k4LHP7Fc7`YNlHZ28f%TZWH>JP%jw_wQ&Si3{q9N zU_ljsoyv^A1emlXh)_%vm0L(jA=rl38j=obt#^(36&{o+@YqwltOhaO;-M+2LUeB;ej>5czvxZEdipx<~8$2#Ok{y(!__H$_ zqM3S|JEWN!E(VOb*?8!>kHN+izY(F*asR@Q_`c#ZPdxs(qmJ+q319H@JqVwk@V2r) zzU&bogWN~;{oSaYwtwO=#~%5T(@*n>MPS*FW>z+j&D8L51@-5k;ditv#!Ue=EJ<`l zL^7q+r0hs_lyvB0$fXq-%ziHLJ)G0ph#!9Gt1C;GHOg6|OqlZMlPpw3C=7&AWl*JsJ%W*~E=JSh z7got#$|6DOljB<-l5{QdUYt^9*`%pmQB}y*nnk-34qJ*_lif54r(l`f%IwB5ZWeA* z-CoBI95YC%8%lQ^NO_P`W?&gWMuhQF*cnoGxGep%@6C>wd+xfl(_{b{WbVOi{N1-- zjUZ*|99&-uFW-9I`L|qq-kmp||DHF!5MDlL&&?0pcZ-Ae*aTiac#nS?TXYB+73(sG1w3V#2phuzCBh=2rqZU zoSLFa0c3UIWnalAQL%$&9f9$EFp-O23<4I0*fiXkyS5N&LS3Ql$nF9b_L6d_858e7 z*%33;3@RbdO_Rq^_d+CbD@7!*KZslM~!^R>K0F8%c9Pg$#n~5{U!z$0Cs95 z;t`~-qd*M-#hoU5dD#)O^JOQ?LFUh$^8sJ|y7~*}&K|?Q?OLxe-}ki}ZDY)uCRa1U z)Clx7r^bE^W4#Oj(it;OE!FKvDKtYXrGT-~Vhz*u%$h8xNLKYUi(#5l8)S238*dj% zzbz=s_Py#BHfv%VHkc-N zLZysnB-dPMBiDA7PHkLSSIxCEoa|t2KGhvGR#!|b>dfFuOqXk6j1`jiTo)U8cXL_F zvKTuz!Ys?WLn&(cB}{D?sila1(7>~N0CKIPRx5NVm<$W&B7@H9 zY|Q+9C|_6Z=~fCpNoPR%MzMRunyatA`%`vyPU?fzu%6ju=o*j?3c;%;nvo0>8OgF{ zpKPPrkai~MHCS+|$X}a&YcdN^SQ)Z9yHW|I>`j|&Q z!l9>`0(DA}>eLV&Gf{RnH|IO^_U-nx&VU@j02DT~HkST+?^@b(6tLfFAU3cEun?)lv$S1wg;SKmsxwV{TP$ zY6cjmlzom{7x@64rwf+vy>tPWSOgANdczxxYbfSy*GW)%=w%<9zHJ8~kR9IbvS4 zm5nEC4w?u}9LlCcv$c7Vrizr4s6uFdk)Sc(c{iHPtN=o56io~r7BGk-_Jtv3ibpyr z6b9ZRY?3lQW^S9|nerUQ8Uqk9!npc`7~=t93>6Hykz>FcAtZsgkSV~ZN)*bYj7*jx z$0#BRW~x}-pkiZ^M5(k{mW70PMI6ZrMgRH`-$N*<@lgkZ8P_}t^! zct_7(XjY}zGN{${Nyeok$l^3qT-N&8<-of%db>*_I|f5Ktg%_=u!bQ)7=C%rZ#}8= zU0l?q%(9W0Qz>G$<=TicJUn`B82sKzCt8Y;d51RNWXZ-GtdH^d#QJML=8%I9%AnI{ zpLyrY-?F!M2~0>%(hZDhduuW^P0s0PRBabS^QFK>t5Yr&Je{b~D2*D-^^aeM={Lgl zp}Ugbvdjpj+BKaHn$TP*hB{nbHW^7szloL${WA?|%2QB<5j5iGR27a$C?Hje1T#7T zBdb*FghYWPYt!s7oANN2rdr9RB!~pnF4;=8h-A1XXQ$;0UnF_2ZWX&a$)&Q=p&;AO z;9QKNDw5FTQ9}l)NLYI!>4X@@n}k zHbe{^!@^FGy{a6+$m8f4)1%rT^H08i=a2s6_MW>AFZZ^xQ)Z{hojCUeFdqSP?mY1F zcka2~_r7kr`rJEiIM4UK_I&dCEB5lomzR1ZJG^|@-m@cSpTG7;mYpDbkvW(Q8hiA* zuX{OPhLIIP(@4?N4qEo2llJnxFXziUZ@Xlpb!X3vJp^8MMhRqj7#m~*KS40y3rK{c z@fcZH7;po@fL2%xuL|nK83UJrdn_RPe>@$U;4SEv9mtNForAk1r&7i#Q@}`hS1MJ+ z4tHV6kTQr3kSYSA!;iot?A+gha$XE3cXsRuwi9R27*Ym@J2eJ}J4D8S&km7gA!Q2G z{J|;b%O1S;O)wEd%D%cFh3>(-n_cbtSj6PvD3qfW)vXNk)MDw*bBJ4K=9g75`E(sm3!^;ek=zL)-eec^}=Xq@J zFTdI-IeFV%U)jo(p4AS{nano^VrXrJ9b>iq=@31 zJIyRLXQAI7pRL1LyAtA(Di`@Q@HOfNWsUIP-f)QH4?S+C-Rx`fXK{>T8#Iu8u zI!y6sy?w8v78^+W#!Qv6Hpqg>T3L=LSeb3T#v)nK$Wjm<1@|MVsH6gOYa^jXpHQ6I zk~PfGXPnTgONU%G8am|cOWiRUEF{$lho(|Qh`3WUs*@<9sk*3GC`$(kk0wT~_)77$ zE3*YQY_5{6iU#sh=U@`b20vwasihjLhv^gmUy&>!VjpFpNL1;Rwr(61bGK-!qFP9) z)aHRxHpxjuQcubjZFg;!JEBT~zD^U2EYg`uwTMJ@jGz5gXJ3(zecf>F?C<9I+nw$O zJ|m;I`Q;8n-KnTgE-R9*_Q;uzV^cR%@2k2+8mNY4FbbZMHD3}!J!Tdipc3!M*bj+T zd+^QVf<68C2BR7I;G{v0fwp$qVOuU7ALq*-6|!E@hm~Mz=!t!p+S|dNZFWA3RHPtn zGEzWhw<>mt)19+@dq>Q6m7ZSqcX)l~OWGgg^|c(4H<9~6jLl53rl_eZZRt>|fnt`W zra5*Ix<%8hJiw;SLq9O;E;>$cr7Y_ZI*_TbwO#Mw!^=vMl_wOr=|s3$+| zioJ#Hv0IJu2q?(xcK`wCF0UKw!@iyMkAHu^E@DuG^-s%PS$sPmfq|>K7XkaQaEJR~ zK0B=WYsebqA}VqrTvoyRG`b?!!c_xcciUwL1~pa}WzDYvdO&0Ko}RG+8ze5eA5S1| zuC*Eo&kqQ?0#8#dfW2UNwDb}B@x;UJo^tmb%|QcOGNg zP&bB~CPu!PWC_ikW}>MEm-TVqhGvI)+)iDzd{Jvqf8|76DNyHJDxQATY*AFrEz$!@wAj8Ur2Xh7`>d56 zUO?kT|Mgz;z{|3DuQQw1>hj24rtmspm4*m1(y`+bxJGy@s~k?X zkc3|ys0nzk*{7Uwmr`{w&{)m5QDJluB8;m{B9%3m?KWTi@-yD}`qwy-F%-t zc6G92N9q-3Yqj@fJgV)tP)}C)%D1j_knJk-%W1Ah1CwZeU7AO&Ymjp-Hl&S;L`x!7 zI_(EVvI1+OX~K};2Ut_lK`;y2f~SMAtk_h@@*-=TH#<@&3`II$MO9d1L}?*4{_!XU zItz&%r5Pzy64_>M)&x&j82IcLfD|#CBWap!;)TQlTBxQpzszPqR?K+%gb|)`PG#dM z&@9WyVDu+7I*gd_dW;%LeU2Zb3Z5OMRncBqa<$?)WvXr&6-O{^y31IEEx&_g38QWe ziY)bmWQR_56qUC86x)3wKHYs z$jN&`JFtAm&6o8&_P6f34qkS|?0M`@y#F=f0uqsgIG#TcC0o}7an++sF zf&n)Q2>5cF24)dHyiA8Co&>>n(x4xPrbp3Wx;_^c-XsKa0d4M>1vX(i0VpL0kSQdA z`#L9;3XHEXI0c7sG|8pFZ@3uvMUbzRha{2>5m$=${5Ei{lp1^uTr09td1o13hQ6IF zgWJgK%t90lFWRnM8Y^N6h3(ef0~1 zzUYE;^Z=ZzX`x-FFnW(i4Ewcuv^RXMuN+5;jbBI^jz)?o>q&L-ObHl3_Vo3;{^-mz zPCMBn*>8UR84$F;!t5hp=X~tl9@W0~it{{)4KLqx?ZrR%_6&ixqqBi&GiiHiuO&4> z%o-_PX_B^-CX{{DfPsWIR1}(nc5Zk?PA5td&%Wvr_tUAIVU_Bbq#FDYXnvgUO~CTJYHf(CQX-i)VyXsH#w43PU*;ib(iM zl8{7TZVEM1RwQ_8>f*au;LE0fe=vQsLF6QXkgz4t4E@DKfCqGr> z$kQ@&;Ir{DM;@k##NTBG0Sta1MUgk=9OrGaWIepYM8nl+hk{`}Of$%!HBgOSBi6ts zhm54|p1yhIyjWcr$HJx5v?W&xf60PTY+9Ef^T#~;(QB>oyGzzzn?8{K@-totVuNp< zWH$9^I`a0SE{q0(aosY)DVw>L=6$+vw3S8eaxcE*f_<0o>126}$8GxXNBqv|XS~q& zvTX0wQrC>u5Y1s(RN6?0n){+fITZl86x^dGHY-MW8R}GIA<|3cHS7!q(t!%e2?0iS z%K#cBV*^V5Sg`%M)x&W- zgzK+nJAe1YvNwWxv@#2r3r)~tjMp?5D2S@DVsO6fa)7!}supPo)UW?NUEq$ab392d za(56vDH zTFtd!M2pug`bv*ssOciV3H$K`BN3zbTCI+Bpd$}4DzuuE2U3p-cdtsFIzgKrVj;#F$6IwG$q*wWb6|x>}DUch)or9 zZDO*SeHB$FH}$e9B$~2TOS4jul(H(#Ukpt%8t{Ss{8ZAw0F*jROekw$C^GB~hQBA{VTHf4iKm8!kTv@3G_5j7NCz|yy~+ww%E*J^n={4D%li@B&~;!^cOnwNbl9{Ki0i z>tZ37#)S*BSXwbg7aEd*>-8QZ*}-c*%YGuL)2t-EQ_Xu9on=?EG2mLexGpy#?=#FY zM`tu!z5P)?SlA}PTg?6zvtN#W`LmxI;V`lh;%SPX{>4uW21A9u030rBou1dd2s0We zu!tI=4&f0M$r!+_B}^ic0@Lt9k`-hfQlHQ)U|CZS)et`jXrKwAy(Fq@sa-@KInrvW zx(sF?PnnQSj+la%`O*oVbixdwBK)GHE(?ewei5@GF_olBi(e@n5(-%^FvqOXYG|~gU}n8Ee+?PPb)Q0*?x&XJDOsW;w-5KMZckH%?kub+itzKS zpR( zGIDlIf`N_+!`ESIRS3h&!D@GEMIzx>Fc^^tUZ`RA@T{gO#cR}@&hpSTT-^7*fMR&r z`7*o=BcK1rXG73%^ktX)F&(_-Pdh=MaD;3;bSdL_Ek#`sz9La6QeBL|vP}yGMN)Eh z#HrKZ>W%kFUd>q@~U;fICmwOo7Bia5Cb7;;U z()2O2Y%D{*B=a$i`W#O`ZwhO|SZ5g-R z37$JnH3Su22-@xxlN=+@xl&<5Xr`?&So_9Rd?THVNgL5W~4Cib_hNz3TCRS z7Rp!NQxk*8)m6$)HZl5Nt(w`*h^8i_plDDMrD$4O*enpT&@BcdONd}h#8K3RXlkM`m1GMh{p2CYae+dbMm+qfcc`N5d|-Bo|9)KgU(Y`D&5 zn{VtLK-*w)n&Qy}2F*n7SYvj&!nC9)B}WwSrvK0(QPGMg0R4d1SfxlSicl4H6d?>d zLgiHC)MQhbd0)BXU4^kR7PBKok7PS2_s68*WvB$ebRl?jIP2A8PqsT3CUL!VT^%I` zUc|<}RB7>RyHkD#yIr`TJxzc0$@|N`V5YkJj#9O}yzCOYYKj^X{ydh)vl|13gv;c) zZZ{?<80NK9YSas#|N9;k_gAX@2xAoaQmCIA{F0#0b&guv&ZCf^a&0kw8eL2Q{=PSE zJj{Q$S=T^vyEo3gt!(1_{jYy{;PSoq-E+6AF1w^du7=^+)@o+{njM7Z1quCj zza*wl6`Q|0hz?;BN3EtLee;MZM8{--)*UVrW(v~;WQ~G2tpn`SVGVCrOo10gRt6Gl zi?rp45UDsw)^cGC`;b)U$Pg%)Ip8y}tkfs$j0$o@Vvs;TA69x#;}gTSxS zs;6e)ft&#`YE>jLv$&S>V&X8O$oxVW5h|rmfgNTkfRn5-B?aGP5fG<{!BK=KQBge5 zu}7~xbS(vz@4owPX&?9klkxY^y7yjuN6dwh$3hLjN(Cb%O-r7p{_iq%-CZmf+7+}s zcGdKG0|2-%PU?aBq?lPFWIw!Yof(Z%WMm@OUCOML-*pf75hCso@5q>;K&@J%3fD>- z5|q^vGi7$f5RGQW=si(I!qC_mSpMO6fl&4t$py0*?Fyr5KC>h|rY6M);*wMsUt3fh z%EhnxXpyedlWu=Gs&z^^%2%B*`e9Tw3nqy`htF5}dr}_8zUrcn-FDr%pFijAzK?zQep?@X(6ZSlvUgd} z`SOb0H`wRNk8zsp#bngsvO{F3nXs>6gVM-xGX+P_(DXA7+2*7}w>@FS6As#a>5fm> zV2h0(v(_39^9MC%k7WOj|2&j8qesl>;mF^=Si1SfzE$$HaYZR@Rd(?|tXnU}t}M*%`Elt|4g3 zp17t7VoRZ8X{XFS7X~upRZ+?aygXBz|1m7SI4!E;(U$s3V99H84_!;aVHnv_GQ9kb zx4zzU+Hkb2cb5IlDUjLszI^pd?=j8``*oK_>3OkWG_!07yGYiqVbCip{Ia9=i6G?B9RZ(Jy-5GkovMTg>ly+iUwuwvT;XfAxhviydC}y|00n zO&pWR2G1tSCdW1@##|$`{cGdbW~@17ezh^HNUepWsHs|!xhb3ebQ&_)grzp*?Bhvz zF;y2O3X4+ihNXz6UfH;0Pu5y-mNdg!KXFXR@`N{ES9TL18_ z_fSia_Tt+-xp^j+o7BI&0IgrZ!u>&t$UEEEHMj##$0WDSu}Lm1h@U_|h;wv^n_7r7!tNSQTe zHH4fbN+E-_np{IjYPedmSs>|(U>GD+8WnICDEs8?rt~vLmOiGK>F8})428PA*krqv z6XI(oO<6|hxLC4-$eHR!tYURZq9nqjk9VhYn{Lk_6_O$rs)Z3xVUToZbbGA{S-{f~ ziH6i~c)w22f5G#f=X+mEH{0Zr3(xbg-%R-Jdtstuk2(q_^4_TTL!JCu^PyC;!)jp6 zn*!#7k&GcRHNYGa7)Pm*>>Xa{#;LTs2?mH$YH9<>#&!@@758PJjKXG{Y~V0@lO-EG zeygQW7_?=wvjeO-skE%o5r!113X`&95{XvY`Xe4*zIe?QHp#Y>w$L`Tz9M_Wjn|&~ z+*6*s<96$;yUyxs{H}Z7D_`|eZ~xiz0~%_nt_fBE5stk9`{p>~EfLLADK)VXjv|tJ zMyRL>u4fQ^n(Q>St0JpuW4_Dgf?1|)bQdK%VHgBJ)||^Ozu0lPKVEa~byw+sy`(es zty_b8x2M+K*??A;j8GU|6kb5oV&IEkwHzxuK?a#!lsXokbzAXUhaXIwCR1ZUZ%}rS zZ2@%34>G&E@#R5%2LJ9?Kl9@TQvh#Yu?F5fCj+p1>dD_(KJoaYd?m~~$bMng!*I7$ z4~UR)O+dVh6JhkB^JNc~`#ELzr##t%-^;e#Z1YVw6^5fX0~JO#Dg4E7ztkK2ww)$| zac%|~)vb*fL>OMCU|yIJB<7JSve8tZ1;X~Fbeg25RXba6D?`dA5yn%pCZj2lqoyG* zTu^BKswKR3s-20apwt|kE^$oBR*MDJ@Fe{iDc9X_MX)MlYTO#+vgDY7bB&xZHIQhu zKzSrmU@FXLMueA1Ldx_5o6;CXA=!qUY`9w~cZxF3j#(xi5i%|dppoENR*P%G!earf zqmT|Y33AMNV?fx!F}&P4vee-+xwNC_PMj&sH0RKMgvww|%QEAbcrL`?Qo5$D zvb5C_GX_s9aHGgDpfJMY2r*LU)ZVaaMe(X2*BxP8dJx7~#Wbj<3S10Wlw2CLkqX0D zEh|U{0iwK2{pP)vYCF>|7(|)8J~!GiybR-bmWM~#^f{svFC`ZPBJaYnxsY^Sjbcz#DC5FV zam2}Bl7cKeDLDyW2$MW_$yrd*^+F`sANm+EON}%{0r9H}BV*}cwj!ybDGm85jOrxy z5~pnPl6NVdWLm|_@2(`Z0=pgb_Obg8zAOpCyF1(SPw}!^5q;I&^)N#sOXxH?$P6P7 z>SJHdlp$pVCU@LCd$oDxm$9Md-d`pGlRH!H8SK9OMNWA8O_$2vdCM2>y6uV^ulnp4 zFFE_xYd-VYkG#S8GQ51uipTrj*Ycg#ftS6!eBiF@b&d=jV?bty%fPbJdi+`^$dDw_~w0LH0}AEPI^$uUn+woL}|wm%xVb z7gP(@;lVLLEQ}@#N5jHEnImRE7(Dt21o)-gm*ay~W(=FbSyZ?3f7>pZNloVQP5Uc`-@g+Jo61w+?Q@ z$c~$xF9)LOgWld^rp$}B2t&+4=31v8Fv1Xz@ccO7z+DO>gT^p2>P2RZlVwQRA6zE! z?Jsy4cK+kfeDvH;e?%3QZFs`D9O*!PMZz*3(=okmB+^$j0c7XwkTPDk%#Ok5F^ET& z4kA<+F81g()a;K*z3PlpoiF=C%pd!}Tb(a^9=pH4>=kBzOv;~N_9C;LOglhx!R)Zf zu_?7jvd_WT2ae6EBAYA9b~SA;sin|%k$fR3YW`A2W}hsf-QA!RnvY^YW2!1ri(isO zX&70BQE^IRDJYDEsp=Zo?oKOs)O@=~Yh6ghJp$i?X~RQkr_}BUgW9Uqd+cMTd^X98 z6T`)hgOD7-b$a%vg3U{1Y^tY9eHhJn4Fvke!k*y22PYvhcFq}sF5(Tcz_ z?-|T5HPu`5F*TepYkC0B2!kQ<@Nx<)u#7Nu4&z9+T6Li6vUoaG;RqE!iK2{SROhSs zvq>`-bW(5+LNu$G*(}q{xYAYJjX2$Iw3k|U%z9(m$@)rB7lK;K(yk5(Us6?Q)^$=m ziNLD6FjXbXZh76=-Tsj4!l0{Eo1j<=oRW@)|F~|BaBD@deZM3@T@~y2l^y*toAxz_nz{s z6YUc`2u;Dw#kHW|9c0yi@0m~YB(|SKfMpn2pTNa_;?Nw!#(}kdz_`}F%tqf;vwwKe z3ri36)e!U0b{cht;Pd{S&K9ak(+xpd)UurIx zJ{Aq5+7ysAjPZuI34(9AkX72$Gk2PCBvmnGkh!LM8&27DFs2pWd^G#~xW^Zp7B@nZ zvAJwoHD|eKqH*2I23h;1(OmQV1|fanQ8p`4^U>@xC1tb52+OJuR12lTo)I2ohF(Ls zVPu+s5(PL~3POeujV0(w5e9%lzz+XcLLrBxuV?>q(j|7`sBH@m4 z_*aSt=OO6KvhQ$QoUECZMWQZcb!AmyElgSr-bxNEgTt8k@N%cj_~}1pYaKhR@hBv5 z3P3ieigZ+2u*Im*=PpF6r4Dxok!>|L0HVf*P%|QIBxc#-my!q`k!#WHta*Y^8c_yY zj-+@*Cp8M1*Yswa+lpWn@c=N07FxxB}&6I6!ij6XALo)FXZ zsx1`MC6O*lwJ77Npkk|a*=JMObx6JnvMHInG*)uo16RYwd*EJW`Cjr^^9D+)XQZatnzIZsh^8I`UN;6Fb$M*JA?Jy!AUxshT6be z1lT$u1?V7RKoSb}V>tNc=B_B9hgV(N6UAV7SdRi^N)9lB;dxOV7~}{NR7(+M_z_q} z;4K1*-CHp}JpJ~!ywT3k)7K8`u)QsR_MF2ItOaaAEBd0HAWcK;5`7_wg#35M$ zAPpCxw3Af64>czKrcrZKuqbIiP;vL3GwM zjS;3WAhR9_FFQ)^U|oB;=24c=nX*pc9eAf%#U%QN1zq}qcfSE-_5e1#>=W7Ve9No7 zybLLW%%A$`yL|P_quSS8{<(pdZN_Y(Z26jKp*)*ROk1y!w!^eNu?cKWQ5$ z5VH*0BXygYu70g=BrS%PFU{eXJEq#`vSZU&gycnqNy29KG1gD+5T@nY0+>6cZNSRn z%L>dM&*`l#fm>3MVAy0wtzeWa!gt$Z9K#Fy_-aYW+A8qM2L4i#|3XxzCd36&qRbY81bSGtj zPPd_MJ>3xT^-M1C^o3MbDTa#-s;*Y0G*N0KG`p{r=0a&jWd$H4P1b}F41H?D6wRrZ zvP!F~Vpo|FA>sWwDB(HwdhWAN_7zrNQnC#Cn@!hVecAu~*MB_XCyd}WSfp-ujb&SIX~Bn&pd|A}6=+CZV9gYO?~Xs_ z2%4U?Ub6oBj)mhLeur(>US|!E*gP9cHI9qqwQ#Xuqq`^p0x%2005x7=zT(PDJ(vx3x&(fyaInkB zzy9+-3c%w3`=^`)!}@Dnvi#Z?ab%r|TnLQL_ms7|V5z#g*N+%3kiSdiiog05XB>Of zVIGzDP`v|U1S5Mu+FQ~tmS54`l>g};f5Uj6Wzz$y1Ix%IHXgXvQa>Ym9@`&;0-42N z7!Yz{^`lnv^#grb&x9j(bOoWPbRC0S`}N+d(9b{h*?!hWHo)HNGgEq@E&YSty!7<((WK*Da$)YAxhUhXbcU(YX-Jq5Rt=%#&aZVcpzi@0mQWO z7#F5fF_gzFVhy^KCMOwQD66iB9gfI@#t9h-FXx>CQ0%8F&xiXl2VN#86t-WbpjWlH zV3R><4`yQ+r^9cd&We6$q<$A7hL?Q-+h3LPYZ|LvLo7*3rWDi$r^$ehVY{Ba&eAYyo`yXR(2TAY1txaDw(Za5pfoSTDcopjEX2p zy(IXuQaqcOMJcCh>5YgoBF_t!^o&YZPZdKTc@QUOE@Y{Lz>#;noe6ItA`@J z(bX^bY!;zz(IJRmn)M-OA<-wyf=FcXE>NvV!ZK2X0O6Kai?G$z67AN2LcSLB7GznP z124-`9>ju*?ibaP#e|oC{HKa$e`Gnx3@Kw|2Wp;T=f3ylEU4P;Amwo&VuQ++9C#>P`N%`K_zB{kK+qSFixAVHd@=^P5 z?h$L~IJ}HM>Co+h%oxC#+_^Kb3^n)6w&$^*dDs)5dBpZd?zgSyu{T)j5s&mZ_9Gtt zV9#TN!2V?9$tRuQhU*Vi_?a5-zIo`1{Q%_ttV5>;HaQNX-NL+=M1IUu4|S{nZbQm1 zJN+~e&xQw}1K->+VLgO#3<~0bneZS2csqyf@RV{O6hoqlEfdtKA}|P6dotYb)c`dk zgwQwr(6khCwB>U;40%Gh7+4r;_SBY_mmLs?g8eid5LVtN zYJtfE3-d01eqmKX+NJp2K$ecj)xygjy#|n>2TXNcr`7cs``j_^R|v?L3!Mh}27YyKk6~+gysXZlhGEC=6@{c09dhXS z><_&iJ?EK6opk&mZ#nCw-e`W`J6`VzZRgGxeD=e=!t8nM{{C`!*=EL6vVoDcN5!eB3Xpoj~ksO}=6kbPW&GZLBU=zc9d+jam!%G!sT326*$OC zwP0FP07e7!RJ6qTW4_#Ze%qO*b~79xbKis# zmDGd+B%Lh74z{nh?#CT}lxMKLv%Ku_OP~IX}@H*YG+~Qnu*p*qD7e^ zVc%*ays^meg~s|oPT6YEvSw?MsU|OZm~?7cz|HH7$VS{nN&`U)Q(NBvh`li$9&vK# zl$m$k?%iQ+XHEC==+_CoBk^kHKe_=^6?3)f4 zqg|@V_-|6<+pZ@xPa{>TMYEz9_E|s}64T1`H^nS3vgVfN2h{>nj;kD#JW12=t`Xn@=Ct19T zm@29j&LY96B6T)84t$L+5^4wv%K}GH!;d_Dmf>hc@F|!aQdVmArC^kboT>4Wui$66 z@<-njkJtpjBR-D+GQ8YV+4vv%{?q(VFoVhd&CZ*@`&|r$p4Jwg>eJTY+01esl4Wvn zx_mlWZ+E?Pau=ej;BpuYz%m$U{hVFdc|%wbVL@sap*GeiKfY;>lu)BoX*gwH>LNDA zro)W9VZe(qqZYL!rA8DY&yKoTt}VQ~Mq1=@XelBmM1rhHwpN#w}z3OHN(z$1SaE= zV`xRPx8HQ}*KWP+D_^?cnoB?NsSmvFm=!y0ztyAm+jSEk``UAdwRhd-QSh?!T6V~=E0>onOb$sV@`lfhxsv)pj8 zXT9iP;AKJvVS5NV(8{kq0b>zd?|8S)5w-|{dNBu?L156>L)Y-KzZm7X8D55tG3R{3 z-_r7mF?2k8VB0IqcxboZ0rZA8>KsjT6|`M5`*DCy6mS+&n@ru(xwCza-DdqsQ^iQf zdJ@~;Uv`fC+}}I)*dvzv`^)cm^Q)j{=gvKn?d@iNNy;a`?zrXZJ8%7>&5xZ-8$}x= zyxorNjcFy@X3W+o2_a4LHj}nwvP#=Dwu4$FHOrczW+gIJWt)ldEDz&NbJh}OmJ4BU zx+tZ&Fy&=REJst}lph{z8eb1IWaTe`7PpqWZUn8Xcq?Q#1xqm8Mf!?r2%FYk z5&>B7YYQPJo2ti*x|A6l!r~-k$F{cD3Gq?ku~xMCU6@j)lPQ|ATnLFIstI|qk?vkb zNK!oFB+N1%b-f4?9!>oYiN^A%sJ_BfMFSNf0Z{IKq{z)nmQ%G9Q4@LhO+~|3Mkp{i zs$*mcYiu!RS&=LY8s*mmgvlBgm8!_EkVsrCcsY;cg#FGCUKX3`&c8e?>dQs@@3ZHc zk6z8+mI6-4N`Cku2Z2IBieYKQn$CuO69FS@-btMZLt93#u?^SY>3I3n*ao}6B2NXw zae*MS2A?V5+7YKk%qiz?yX@j+=QUP)gc}O-i6?J8KVj=FkgpUnd;c%~S}lN!Yje;I zoyRd5TM8)HwR+0hDf8d{95(y0w|yPLI*Rq^vW@y*slM`r4ntFrrW;eW%QS2b_R~7P zmTG>KckXh{qfRiRRZQ3{meo>nrPfSs$B>L#xf@RDM*zs8WN&O&JlpWk&kfuB11b8D zO?7pVi^YXL*EtSw{k9-H-fM*z#72P-iOr7^s=Iue29~0#J7-XNPxcgqA647CqBnz~-ay)J17F*%>J?OiK!;U?8Bb zb;rAY2*Ma4pcyde8417nV2o`9E2VF2qi~Z_RTxnes!A@EB}e$`hPEnVkOJP2V?;_5 zGezP9L=OeE0*@GVxfTgNyetNIlR~9TWsM_}S$N<39yr`lGNkN?S(9{nlqi1h*;}1XEc9=H|xVWny$!@Q?c>JovFbZ z%Yu={K5sa31kWC@j~tQJ!v z7(zu{8)ITfju}QsvcRc|>9dSSnJsum77`sElVj31dTkF{pqQ#5EFqTaKasV0M2f;9&$M!HbynM&am)?E*m3Pe{vyXk9bky!UY`xk(J1;qK_l@`1eyu&W zTm7KjHiVZC*>eeW>~U+~!A5-+8(2oraW67=xQurU?Zg>gcE0@dL!Wr;iXHaaX^V~5 zn|(E929_V>dtcBf^7o_wKrbviEXMZ~w6~I9`LY+2`*~Rk62gqmp#9(s7yI$qcThd1 zRDlT8w5)6g^kNKz4q z3iLsa6d*)JVR=IOgz&Oj5H7q7>Vm+KvcLEs>q%@+aKX+XGZ5`VUSKjrj8{7FwL@eQ zXU(87jO=M`Mm&ZMddrrE(?u1c&#wS9#6hqp&oO=<0GVNYk8Q)t$OG8kP=<@0FFS6A zp!o-uy}}GB2bRm)J!&t=tX*KQJ&dgO#`l{*hs!}`MM6q~7fBVpcZNs$q_caxKK#tt z@AcfZ2eADyDd)@HU-q}9-uA{h4hnT2OmRI#p<@& zk2tGk1E~o%ka~k7ROi$`veOpTu2Kp}^$1UyP5Ru`w<6?Jwk}eN&kII&7%ZyvRYCY^ zc1URQs3%##v&MCNjb#>*YnPt_6zL_DcQ&bsr+ZnwGIJF1Hz zg({Ai2SZ`FR@bdoU1ke%^adlII`AN?7<_QpkB4Wz;-x;Ibkq?C`-#$mY{~;Krlg^W zX%R31n}$YjR4H^L3n&3!ATV6x;L^Z$(CI}~W1cmK!Wfz@Dek#+9E@7WOfL5tA6wpk z-#z^OAP@cSw8OTuhi{*IibdX`C$P-8)j$A30`64dnnY?ok_$izN;p1tvg}|BV_W;v zzy1rr*$c}KlmSc|cw0&jj{3f|J!i9QEFH~3HhE#3Jj;x0xGZ1NYUNkcbWTDQTSnAX zs-;M)5r!$k={hF63M~qj3GMs=7K91lv|HvE%!GV}Au2AAFbWt5)!`7dc;c0I30zbC z?>k+(S6z(XT7hnD@hY^(z?h}!QfAQa9-b3uuSG{)rAYj>olw)d+;yO?pg_*P0GM(S zDFe$s2vkaL#X1A$CXf=@K$U4B33B>}UhA#li1s8~dHGv$@1sxJDaw?T= zw&og7lVzz|vSA+=9Wr%LYxGJxYc`nSWk?xs%E!S5b8?T-?SO262HJu1d#{OIbEGDV~BzQZ6KBAAs=lt6cYXAxfvsEEp0+ z25r7rKpAHWBc7t=RH)pM7pJr)ND2EhPeV9@RHge%Ev1|nng;N!_f~@9#WrG!t*#9#i{BhHA$w1zUoM}%TZtjDNQZm!jvOsMOfXT{J`q*|Na0-({Gin ztJ7N*KD_Lf9SxW)i?;%YmqW_3j0~Z*ojl=q5_^2?3*$hU99Wjh`ZH4Z-gPUy?3CHV z*a75$m!akm^Z?6&XkgjlvhRIioG*JG``+8H{N|lsyyc6Ze$@+)-(}l1_uXZq*~`mM zUdQ3`k;^xCxa@mhJ#h^wBj?C)F$wbKa!1eP0Cfl&WIp+bC;8;p;d^a%#PV(S*=6Zw zOJ=W?Jp^8U@oCR>l;k zb)LlAX4#f}A;1}GcIZ4F)ckWYdg6f6D?+rnqM;@vs32YWybg#HsI`t z*>N)hoEnML=89GKji~a1Ap8AH@yg0Dz-1)=rd$ZE3F8wT+eBak@{KnUBy7$i6Hq16gYGY#$ z*2d4C2NBwIA$uF8$X2KAqUnQaSBWu|ssHMA<(5=DUA43x+QY%S!0oKH9ffbv0+@b6M?LVot_>ZlH;So~+e38fwVrmc_-h~8f z3x-ET$T)qxy87Q;l2i5-5f%&uO!~ukO(+Io$l`0!G)|IS3y+b0f)GjVlGX#Ri;wyE z_P!!j#2{ss0?SIvaxwKw$R-IUgBaPu#;{ODyZ|&o%A*@GQY9BI3d^z>lv!b;HVKNd zBgMNMMu=38F{p|YI)D0eHco#0eMiIYss^WF3FJCAoNLVxAIxGJhFqjU8?)O1qAcVE z6TxMMw|9`6t!9?*Cj)ep6+uZxHKwe2WR1!NaP2gc7g+3H{^`F^6AWT1*UmSao&cBX zCQf&4nuWxgV=T{O+u=L<@m?oOhD3g=Wtn0K9)>BVD3_k!O2gEQv%pT*jz@Mp*zbfILEn(I%U!<}6$Mp7%^JeKS|kh_MT{Gk z2CY%;z)RZW3%T|eqhR6Al;L9N7*h69b4Sd*w(Lo4I2wqaz3e=H zCt5H_FtXW~6$bkPE`C{#j2u_i71X!79q*Fqa97H;=$7F^yJQwaL#VK|C1{K(askJ4 z%7wJHu%i|W4I%asz7<<3GScWo4W5*xvg#@_5>gPJz{JxbZ+$ybP6uiGG)~!B@bg<6 zvljdw%n$%;B*G9CFCwJIuO>GOq!h%HaNuRal<603y@W?hF-Rh98d4L#em0(w?qx7y zDylBCjeIG2y+o6o1w@!}UhKKSi@hs!|WoS(I8*XtF64q>xmR z;%kl;DI(zGO*lx z%?_9E`T8yXu#}gV$$#?wJN*gfFfy2o93OiI8z@E~bKlJ-_ZIUVH(%!MW|G@(yvQre zH(&EP?=N3_$wy8<^~l|~U)S^4&X>KveDEF{9k*g@FE8)E%Q}ECM7&~`CH)EJK7|c9 zcfRa!IVjzu+CcQv4}QWSyDr&(r}Yord)e|`x7zZtYp?dmM?M5zh5;ev@G=JO1D0h0 zF>u(YN*xM2KMlJ<)fFo$yAqe~~1u6@MQ%DmZ zjD@LXK}Y~IFiRd|hvthk5T)QjQJS!12e_(`!spuyH%F>E}Wshfzla9X{ zx zj_i*~o&JJnz4jH)_dK?zvVHH%E6kUifA+OkoaawU-Tl=s{mFe_al~x9VrOOF(hka| z$o>aUXsd%TQ$C@&WB47xY)A8}x>`&zh#`#6 z_}I=h!HbP43>rdevZ`1LxZ{+L){)~a1&U@tu6&Ng+S20IveR)+QMt)9LpL>-lf*nvI0;S^75G;_j*NwA!!+8 z6V2A@`4gM&;J`ws4{qhKLz{>0t#}GHMQt>ZZV; zY?~7)rwBvN2%>41;y%ufqJ((cGh4pmsiIvCWj%@NSS{+dt21@46p`R{tRB!0^L*7pIG;X*FDs8bX3W|9%@UGP=>NF{9%qE$R zbMAC$`P>ml~x~uWe?B1#`im1vjKfb(%;#+NH@Z4K)gc z85gHvuc+#PgLtaNDa)$Rq(-NnAt&K_T#=Z1Nh(|a>mR*`H*WP(ealofdEu1Dvdm(5 zahd4IN<}_&$Yo8*E)&iAs>rpMDI}{2B4mvMWfhB@$6BY9DHlS4i_EE$l7|N08TSmg=6Jz#9@)bxW(}s7tv8Jbi5se%4rgyqx5zAJ4#Frux zF|_PsP%%hZ(4wXx1zG(di-wo#QuZ^KOF-YN79M`?8j-sYAi0Yo01uGp6IHa4!f-7N zHtS^L)^K%c3@sO>6{#6S;sSNQrWRS3j6fIHXjEj{8ieqpp=*sw*}WTYJiFYjCv-3@ zNLAzxPX{%m*~ywv8_6!E;5E=i5v?}10%$M7yY4m0(=zqBhU)?CH7Zmj^kle{LDi`d zY7t*WQb@9dNHAI<3~~XJlN6`YMzSovQoP{t2qUYCy0u6bAw?wQLv!?=e$db4gECdB zi)bohL@E*sMP0<&LQ*uSFxaW?@Q4wltHV^+NEH%tB+mMwXcID0yK-HqB&gUlfleN| z(@P}kDkW5lO}xg|ftY+L%^g$2H3>P&Me?$Y)H4`FQ#PBj9O)vB@zI1p{GE{nHGMs1!6RY3`D*U3ZEfnq?|UKL@S zDH8(Dy}1k~hm;jLV*bIm@A%#~Z~x|9U%uy#>k-B|9rW?-dv3uvw!?h?TQfBbh1+kq z@@qF=^W|$k?-SX#e(7`feD&h1FM8jh%eUBm>xUh@&!)$$*!qCoHe9~rdPnTD#Z#AW zb>LGrK61~cCmgWt@%wLc*lwF2vg@YDET6rkJO|5Gd(-7xV+%f_p%5 zun}^kQzhz&MOf=r2k*q+b0|5A$ZU^Jw2CE6o*D-2|>oq!oa_(fUAy@L1QhYtZ9%eUmOuauOVm>SXf08q;8m< z#3_8wX0uO67lGJ%59yP#x`-pnI+i0OPDDSD&Z!8cH9=q?vnG(h#h?4+$ItuJ+2^10 z34|x{Nwtg4{WNq8G6U2=G_Z^zIro$A2bR6U?0nf@l6w8CPJ8PcUJf#QPWzJ|dFQ1U zeB!2SFZOY4e@V*Fm3BH1Tn(j~8s-boib0quPrp+JICOwkKs7*0!jxHNuF-VRph(t~ z3@@uof~UY8%k;y@B;-Y+3O+TGs$_OrrNGEBT<5)MUgX1_0LW)L^WC`R#bVeB(~8&1 zV>wbZNY*`<8nQ1!Y9SjuMHs1t&NApuN=VH@M_3gKt*hNE%2p&*ltd}QBX;oBq9fWi zlkH>4ZLVw~y6xI!xzE}*xG9rh+Ksd!ar150M~?4aJoYE;dB!%T?IO+g9;~I0VT4mf zbZlDMIFaXzP?0PhxAhzqq>6|{X?AQCG_bZy@nv;*eTx~rK|!lE8KqoNRdh0C@@_F* zRvnl}A+_I=)*Biw5&@ueFxpaJ064R7DIOgh^Pb3n5E+=wN1DG>=d! zT4T&z&uA(5+9g#aa!t?)xz1+xNrcgLVIMKuUX+@9Wp!2O1?eS&7O`gV)X9Xhgcw5n zC{;xa?4W62b*!R>{he(aRI$Y0m)hbd2jbKJR^?+0iLXv8QzyyP|LcmIS7nt7%M!RZM zO;#~|DZW(GH zv2?_l5krx26&ph7D9TaQDU3_Qoo;cVX;PD=!zR$Jf2b8CgXBsZQ3+Q`S)fM9b)Bh- ztZRpw1+HL&pTw9q+I1Z)sSoj$VqEv3+0-xwhGbZyM!AZ4p+ktLuebR^O}Z5Df?SkD z8x_2(tobx^Q92Dw%p&)TTt~fKTtbduB3bVjD3bQYY08DfNCRdT%=|Ap3EOJWf@atw zMXe94ku!+!7DtT^L8biQa@*6nWe&Tv(Cxz(lb z#E`y8AFu5)~=&{HuQXq@N}56oya z3-lpn%DT61)a~p*%^;Qd`3+BDWC=06qtQAFa&rHogLO@z<*G$fWOlVlT9NKz8n z8Y_$E2&pnCp%`YN&hfJCu3a%28}9*@1IU42n`8=jJMHu_vZH#NaA{azu0Q)@QqGsb zVT9?;Wz2aeC54& zT>Z6MJe7Unl^4F}s1?h$+w%WXb{}w`Rn@)!5d@{fz%b0vVSr(%15Be#V`dm&=tu_v zk#2$55Kyd%4HZ-b>Am+BiV7No#;A#=+B1Y1-2J#^I~~3M^dpw;w)gH^ zZaiVMtGlxQS9pFI+=KN1Mo17U4BWwbFmHGn=7mq&+XW#7NwSCmJuwY%B|Hf6g)T9P z0hO?)Gtb_j<#@AW&rU?2`G!+`$K>==PTGIJ1-tGzZM&_v*nXQWy`~jXUVYV(nLcRc z(Whn>Y?dWx^o}TuyG60|1ptQYdfK<6SVzscd%p}IZs$Sd* zO?&fahnCgXrG000Aul7Gi?FNCYFO(O zG{eilG8wTT)sxG>vR7drbNG@soq576=$(Gd z@$-%yVSCD*wS!Cpo8q9T0fHD$gRX^uZR5)r!~~FOv!nuqjXp6u}RAzb+5av2}Ljln&)TAet z+LYM3&aCC`DVUTbnN-Q3=`s6}Y`u-?60^Kw!o(W77)ah~Bi zpr&qCq2vpMhnsjEn}u}BC`q7mKuKtiMJs}GNr_PyZtBxN)Q-|_ak z_V9)v`@{F&zg~SSsp-o;ZH;YE9(t|I^k{$}tBm^i{X^elsuskd&9tfM!5>qHvWE6WsM-r< zz4)sgd-Przfg=GyfMF!-_AZA~NMh>tm}q=74b8y4Wx9>z(d)FFN_GJ1#lPP-P{#fU z;QPzlN)&QMexq}RA;XlFZ{R6{o%BZ2OI^B#)8U~H2ut6?aT%&Y*j5G} z<-+-!zxoT8A(R@u>{QA7*MITfq{^Zo@<6m9(8y9EroJr~x{8bQjCN69bunKJ{Y@&= z3=aDh0xtH03C=J7{%7``RZwNsniI$bpS`O=&!mUP7-JL<&@5nu86im(SjbfM#bgyu zxugR1D%(_$lE59ALDdTN(^;UlP{vn^38yE|$b!R}3|nCs^dkr`o3IgAMW|x7#zH7D z8F6YAftI9Qw3@u-8f+t`c4dstV>pT+FS|g`kbU3}#;}JZK7ttG0;Z5Nxv@f(Qjon+ z5#spd(ewx}u~t&;MHOK>RZUsM1|?{mk>z8bBxr-UrElxrc9!wS<7JEF@$2eV)HGn0 zgh&9h038=VV_~>#qj@}D2Bk-zVE*b8P=){+!^_|P`ZED!Tg;Mh#J<#H=Z=iauRZg~ zSDt+EOOJc&*Mnbt?8Zm$`^+s@zxT)kcievSm3G^HwY_&+4_;ol$40{9S(_}|Yl~&G zHa}#asrHu-n!D8jb2dL@-)%gk>|4wS?X~HVi>BINK5FqcM=zav@*z9D;m8@nsfX`& z?EX6*vEO#f7fheI(-iN=9%1E`UlA{d(I7aOPJsLDazd9tr5NICBQ-HJl$Wq1XpTJa zND?z-=Mod>BS7K%E_wGmf9tKj?&Q&6FWTy@T%P;8^~URfXfN9G;P53EzT@rZzxk}w zPqhJk;&I1#9ru0<_MNohhAbaKZ|4q2W3ILOt8G0Gsdl1$g5;oO`}v$`ulx3044=xf z+sg)f-ynarJL>S|Y*P_QcAPy042OCsn9X*3?dl2vUXT`g6%c9%3!~+78N{ZLH>xH$ zEJ;i=G4lM79w4`!Yakl*mdmdbVopO4784dqB1V1y$Ql9-L36KiKgDBSQ#Q7V3=sp! zmwosH_N{FtOZvoz-Y*qD0eIOue}`m&1aqXwl|;l)^kqw@bafW_FfM%<2#J~ z*aNqC<(HRZkGk&Ip=A##gSu!#45W>QH-*uj?6|YpM&E!~(MFR2XfOiHK)3h3$P2Mb zGQPwVCMFKjii5WS+mP=tNR^d9oRpA>sAxLNHI(8~u0pBgF@w*|1Nfr~zBscXTI39? z#FCnM(C&Kfcg;)Urfbt_OB|_|90hv(rfrqdjhhC- z?!tpu85M*eBd^$!Pq6kw6Gpi8i_tA$2n+7=E~%<4N?PzJLLEn|$}t2a=6ntjFvTu~ ze5lH3PUZxhAqhe9CH^cIF(op9wSsbopy;a4ax!R~kv(H?pvBB`gzB`WWk0uwF$a#N zi@apWqb*vj*_bL@Mo9!=|G$Nf{j!m7$*Z-zlu&MJsxE0LBZ2CuTmp44 z+0{#~$|cZhP4*O495G2b4MBK?fq>H0$nq~?+FWWo6>lE`7-@TPA5*y%9cSQIDW)O@ z+jxMHWs#Ke17}c$BUc(!XMErULEZC}Bzne&GNwu_NujJ#mMrPi1TI$?QZ_6r|e?rqq^>%XAmXc*ZRvrE`Hyxd7_SOZvwn(ZuiFdIhzli_8- zb~C&TEI;$e-M{g&8uXv@r@K zLY*+3OEh2zySXe&d!ur}p7@|8&;9oL% zp2z6d7R2yr>-P!qRL>T{QkHl5*RNIAURj&rcu+purAdUPuLs6!Un zTz=ODXNY}&*&%JO$aW+fQhw;38}7T~8n4LKMU5$-(*6%DX*d`iV!(4eMjlT~A}?ST zx&)lVr{H#hP9S;!whD6ZB22?Tlxl>^u4o}k$OLl5WXdAfP&xu%h&c%0H0sMW-a=Nf zr#oFU9@?F*nb5py4#aOxL*utL?!HMNjujNXNfo~=lvwt=FJoE~$0vht1~U_i(E_II zX>duQ;<}00y0XcIDZ6_&+Ty(Bx40WR<=`r?;J#^TAw*bfSybQ;t8sk6oi}7h5=~}2 z;0<~Za~~H7NTq=ul9pa_xwBJF#wB^TX`zf_@=`4V$+u`BCOd8NN@^8^BjDg$pX5Ci z=RLHlVXB(Y>XfTCBuM%iVWURGz(p-2&-J?9Gr}oMfC*) zNlY>pDk^2^Lb$v*mN6=oyy{q|Q7DU~d?g-&Bu%eVF)TS-Z zOD!Wps^L&WS8++@Dq{+1Q$+eTWrmTY0c}t)PlFQ1MCV2wiSuh&9;lCTi?$H>4M(aV z)g}Ea3~6H?BBv?H81z#B12Dx|RrV@dQY(`xMFWD2B-VjQQcS`M|u&1~LC?GRUm>QXl#Gq_i|Xrbm1qY0B?q~KC61yLxKD^k@$<}J zzAC7@ORXcRBFbo%bE)FQ$h7>UWU%lhgj~cZ$(b$*xYW;8+_(cMga#lcyb#TS#vlv= zF9_i#4wRa28zZuCrr=VN?2rhoA)`QpQjBYXfb8VcZxYaXY6x^zaX#=RAXFcd!D=V4 zx!oTe9e@q=uu2gE&4X2%xSpv{`duP)X2Oh7Rwm)d*lr_2D&xg0l%2ICjRG`zNx?CM zBntDD%D&*7u`e@urG|Auooe!LS&KrFvj=Viqv#WRDD_yOtCfCV0 zR2)+&E-~v!`r*)r>+QCKZ3=(?FaJ9T_^kPgKm-6ZcvSTKIFEl!&>ey`H2gl19uYx?II?%5A;z;sQJO{9ut?nxRZ~y+! zD)Qr>{0;c*9^`oTum9y&D*OE({H2Wl@~{60SrgL%$m6(F9NQX1>}gD4vZOJH6Em?a zS35HJs-p^=HeQTm!kChVkZ{w8TrPziJ`^#YIoLxRd9gyv!Zx9CH0U2sWCM9(;_M+` zy)vw_Fa0V5;u z3!&py;N!QFOC{N|d`N0TIj~$D2S9d-hLMGKpFQCGy>C6|^fg?J_>*sc1q~qEQ~vHB zJ_{YA!Dp8QKxug7_nv;(Bh2u!?@2v=|JC-FpML1d4_$nc=a+ZdX60EsuDRcwjStv+ z^Zn;;dcfXW9=2%vBNy&qe;I0CvWKrqO+Ik$7Dq4H;nc(SJn^8NeP`Lz%AQ~L*z%G4 zZ6$bwdA~g-cr)1i-8PuLv*TM7443j5Ih2pDNVB>2uFI%U4J?-(-^w)?3eq zJAKw_;)DrPCQaI7x1A=eyYA|%tqMVRmOD%xjvk(m_GC2F3{CI4)ApEwXt-Mzl6%eG z{rF>!0NcUmV~#p>*^&j`Jl2_N?*>2lgrhxAeZsLvI^fMPrgEKHpLzQ01$OcSJGmtn zLA(6JX&ehO^9&+y6CkWcc~UNtV!=UozaBU=?s&GMWru~~W!uUE0$7HWJ-_UUW_0JV zfoO02f}O>VUV90)bJsp5<&#q9pZod?&O7DecbtQ1i}|K&KK1DRH{XBfwc+K_4?A## zN2SKzeF|d^xD^gX3yAUb0C)qcF5yrh7h)YkSrPzv3t`)sNrr8Qq1uof@Um1HlLvcc z37Dg^A%WqNBzbd1GeAXlF{2)$MAwFNN`tefoBGDSMA2y?Lex<;Rt_?ZJ7mK zUCQnzNKzJg$s~_&W+Nap8BctE&f0?DLGjOl$Nk39MSzKn8VT6($5%WHKcA2;}CP14E^MXEEKxGiwWq=LPZmkOO>^kSYBD8)9I1{2`iC2nkuQ1 zG7PgYcq1osNFJ-bxQNf|y=V*R`iXMsp@Dv$p*;W7uawF%zMQybHVcVmm0TH2Cfkyy z1Ya#&Qbn`N0vacmq6r9+R0>o7sh_Y@P#smkRGlFZ7(%8}sKk6r3EQfO=57TjRVj)? z6o&TAhvc(FoWa>_=+zq)R2|9Vu$Ik~&_68IXqJl@jyOp%F-g>-vmsS9w48f+Ol*aL zH)>Np5j1Wb4lx!?DPsH}l32Uf^hxG7o*eoKPVzxB!eKXrERrA;_yvI@qshu#p_CL_ zgac6Ek3Ai+%Z6Ca%s;K+(W4rTfn_=abS%W7mU0!zDwnLvNPLp`3MIe{0S>}3L4idN zK6LLbx8CrDYp(p7#@7Z4895?aG9`?-I@s))imEM<|#CDc}YqRnOTD9aeUrCG?HoD=6Id`bCJ%mxPJ0eq_1kQvdLio-vZQeLU*qQ3B> z+t8V3z2W1Z_#IBrF1A)B!G_fGiKE&K04HgXOGwXPDyXX=bU)3cLqfYh`P=VnIG31O zt+_}`X;uIkUlxGZC9q7An9Cph&;Nn{qaXa$uYU2fU;gaJ9l&--8z1Nva0Hi_K0Sc& z**?Lg5@q@64}UPy{Fi?Y|Ni|y{GEURlifpayZxrOdFJt5zxAn4fBg14ZvOP;pYZ#@ zd2c%FvQK{W-1E*=zl%umMypLaX#OSnXK?Jd;IBF%a#3ed~*^I6ttRf(lo;U;s&BT&StQsXorzBOD z7sur=n^hua{EQi&iZ5?ftGI-fTQ$?!rJ@n4X8sWq7*Z*5SwLsD4`CzQ@_AbwJb$Wi(m{J1wP?D-*!Gv<`Lm>$aZD| z2H9~WH2@7dmyKM5w-5vl?Ha8OMp=@Ca|h&$LoCc&u28TTU#jfMr+Iuq%WEYPjlS8J_v2wN@+h-hM#bHs#zXBh*+Xu@Kel|rp0L6{{YOOiSY zFTQ}+eMALSGgt6Gn%nUOG*@sA83f1XYDpxA)=7ZO;9HhhRw-AZ;%cG3XsZi^8c7<= zBTPop5Yk{hMp!Do#fep!OBr1f7$3+8QZWfc=SK9p;D?s zwV(&5g{W9TRZXEeL|#gCn{Jwzb!p0#CFUps*`QO6m&6=W=|Kx+l$RbcPMYxr8c0?s zf$@Q63bm>B5)diZh?>5<1kgA#;-C$XE|o%gS=0heMnS|Xjw^)m{^uEiD|klGz6odo zjVL2+gQ?Y>k+sE8ggP3(HkTR0JYr%+w=)>XNX4Nq`xR zsqyh(zbX{Qr^=-i<)(*PG_}J5DSkzWU%rSMk`T2!<7-WqCbv8|CTPc8fr#UhqlSTCJT_8FElV9>@F8Nz zJ}AVLF{p)*5e>n`Bxa5>=UGkm_;VIbJ#;b7eeRrC~6a;S5 zMgz?VoXSo4x4#V;!@*zw<~P5A{?2!7DMQC!7mp8;3Wm&hC_1PbCY%@4zIdh@1Rgy_ zJ9f-CkIBv?bwQLwCbjqjWX?qyj8=X2!+;(Uy7Kc^_@My0lwB6pv?puGQvxsF|G?ci zptRIN`kBhZ|NiTL)Jq{>Sk;L3eC|K}{m(Vl&wlhbz$}Cd>LSGYvsTlHB#DtR(7gWL zn+n`+E_m~~!u#KIk%N)9-+Y6|3Gch>c6?KW1??M8J^8}7Uw~t}uDRt!zR0s?o02lub6;W!LFbx7~8h zwO+H@YgXQH;(9ynJZ<7e>(80H=gzzAuwdan#~uGVmxMQ(G-21>c64L?(T{&%6!`POAJvV{9^^yi_&yKF2EObT%t*Rm9n7dIB9 zDrK-_$ZKd&m`PIQ4cp?ZFeqp!of0y#3&TV-lsFhbCz?#IkgFna(k4bzl|o`!8{eh= zz$YmonHU)zql}qR1*HxS#AyZ)r;~i4qBCjmjJ#P(VP=e`jdWluhQ)w7RZbI}Dy!pnQjSZ~41iSzc@zfFzT3jf2k-5f z$X^x~yp zA9(M@K(P-WL(2A=L1Npbx7~C-$ZQG{L(K@OciegdGbn_Y83S=Ef|0;G4q7QNCgScp zZYB&%Lh;T>1DG(rZ$dG^lgN)ec+c&(T>sGhclog6y?5Oj-1ebofYh$;aYrA%>Euac z=y*M+vVrKe*LaODSlOy}2;76u4zUj#)3%=1T7C2x>q3`sF_;Wc!^>@3^E0LmYv?-; zdCVQBZF9&$OSy}a`GjM^XLaO874d6;I`S&t3gKndC!*;!`L7H z@VoKvxao7ZUVnwRe(9y7^ABT>Er&NlrckE=Vp}#QVmu9B`vxoE8E6$7o|FKmhG!$o zK%+#GjF{-4YYNc{!VE0SLf(kRAs}v;H^yD2XUIEVrh>>9B&A%r>A}Zz$w*-YoL@ZF zYmFCRF&U!Ga;7YF@J&*DQxIA(*9oN(#&@rD1C`hOW}b9+_Oc`QQ#46Qov(IDvW%ey zmxw{Fi~(w@5Kso?X!gi!c`R@8Xm?3V5qC@r7$L%aQ-~u*1FaSnfnqWi8^KuX5KA&^ z9@v?fC5k0n^2BJi_ON;(fEEZ)C@+{Si$E<-kPTlEgk{MRG=)l`GXoSyiGXms=N8ph z5fv13^5AC#PSbu><^f*flTN1-sF^TxPWL7;ZBpf`MvN&i#v+PULyXU6)v5Y}(_TXG zGfwrTlLnW5E>Ll9ahXnPE%_|u@N*-vn6*?l-#SmmGg6ClLfwGbT3JA7<#JJyZ~2hl zT&jo?l_f^UQ4r1Y)?3hePNm=!Qakd@SSRl$?)KVEIr$9WQvT;#hVi+U@-q=_G{{>P z0)o7%z*=;95iWII^30%$5?D-7v{EF=QeHA>ftVt!QG1n^)a*eB^t22h#s(Z2xn&5P zGlbfO_k@}Tp<52i68%bc6`@KUU1}QZ5OtxMdF+>D{G!>M$E31BM)JhitX`^&&s2sD zu0snXYX$N`*;~)N;J~pCsU3j}-G-5Rmp;kn%pS&_Ru&k{5=GNSgF=OtNt7#2lAvgO z9Ayavs(3}?XH^1{hM>MGUM;vqS#-)i=&7th509ZU22Bsma)~J?95Yk%hNPE=ypfmC z!zIt~Niz@6AY3-Y>ioQf(6Wi;x50ZxL_um93FGi&Gty|tym-Qye&eWs8QiN-bYP8F z8LuL_2#v#R1e;Y~t@(c#%PCAMR*tY}AML+z}_aR3o zmIXAm^eA2Dy#5QHzx(!E?!5IT#~Q6OAH4T2J2=)}7NvG=F1`3dXCO~G@wnY~+QBPl zegAas%-#3cb*C*io$OPm!>Z5MTcX!p^R>QwiuS+phU>4k@=6=5yVj-~PaJ*QWtEkP z$+gvHQ+z>{hRG8r_);pd&8JL4%LwhRxBgliPu_6)j@t=fFI>FpYOAdFn$@=6X7d@l z?>u?R#J0Dl?J)IqM;~sZdFHGcekxI4zhGhMy>3m6gf459fU zg(d_FWh_f0s9A+r%$LVa9Ta8&$%>FYo$?Y&Mnac%K&nh2=W4=o#g>yzeN7e{{*;bP%Wzw=dF$pDDgW#ijo{>n2C z3Yb17^{wA~`uQ(E{G}&ud+4suKKIm(&pmPN`DY#AF!sEe8#;`=*RJb&+1C+^w}+SK z&)CSe@_sWWi+xq<$VJl*o4>8|*ry#n^SEWx?JvX23ummiaK^ezXKe^9@4NGwOJ_}7 zzV{aJ^1_*0?7H2Cn{KoQ1os>Emw{z44Kjt)1PD$*z@A>8Wk=N^XIsqhDOd+4!*?Ap zhJ_tMhGcO@IPQwe&|)YR2LOay?OcP&x7={;J1%(3(MKE#4#UW{n9Wr1vS)*@z3OwG zU-l|6!`5u(m%5-gEGe)7u7uN-jumGZM69VKQj;+b3ohHvq{x9bQ{@TPzGPydY0?p1DALx+txG7+0R;b!qK2H zOw9nl31ls#?8gU5;bp?kTHpMI&%?zX#`aolNErt-2A{p)3!jV=+Bn_=_S^4!`z_a9 z4kP>i@)64yh{0rd*_*z+A{$=5=eDbTQp%T?>!QG~@nukmZ7~l(*myC@!l4ML6WDZY zJ3cf@UO+S$2^502foSrus2igC#gbTsX+6$z zGO{4Zs8I$>B0wQR7L$y*$if&Ru$ULO?6>4I;h4cpP(fXMvyfP5$=ci{HoxHnKwP#0 zcWK5mwYrQ`F2$Ktaij|6YMMp6^@vGYaiIyD713!^wADMN60K~33o8n%qp+Sej75=U zhI?!9P~7s5fBP+SX9g`zFn zm~GXebhU2do8=iMsdX9-!g8yXVar|hQaj5MN+c0LS6OR0lD0x9L`apSIBN3)&6ar_ z>;9{)*`^9A*vcXJU-wWisoAe~a><+L#V!RpYXYS!NMNwy6wMDDSf7y{k!NfoSS?6W z3g2?LEfq-$A|o&+k4ehIEUdp}>TvHSAjoStu(n=Wi%^0oP>2IgYCgjK#1ZzyXG6_e zWeJefv11bJE0b)iF2vGFk{E3Tx$0o*F60XC5^@o@@VQX?j+$)Tv%0Jmn5yCt(afWn zq{cG#t1JNo<5e7=sl;&bNj6$dCaqP7sRbidAuo|f>%G-TK?tp{b262Mj7JE@pkXys z!NAd1YhO%-Y7`}=if3p>3-r5GbY5kcp!7NbQ)QH0UXpSl1pF$3u8zDxo8D4Kl}kXg zne)|@B(*7_e6T_FsU<@mOm zn*%A~UWVD1WV6a@L^b6xG|OGqzBK`wHjvhgp$Mwjqg;k5LRr)bS;&6X0UPlJw8;cS z()vv@VZeqwQ{|Gk2@N2nmd)xSqe9K+IAmNlG?Y$Qxndb9S9isc!GSSNV!);j$-O`S z#ZO35LdI-qX5$~q(W=9K(3NI~he0+wytm$Vqa#Ull6jw}#ym%Vy2qecSD}+QN~)sapZClQvlI(1VuE-eb2-CT%!>?>X@7F4MQ) zdh<=d*fmyLg#a;0Fz;rQH(K*Gub#T)W_#|oGt|88R+|I4B+IBnm#vwYZvhK2j? zwRpk4bM~A;gS=2So$`Xo+i$a_?8?H2qwxXoV~#xRO=q9s&9_cdZ$E9DNgHnf1<&1U z=3aZxI{l5OaMy+Je!HXf&Rt)1^=EIs<@(d!aPkQ!9;0?Ov<9kI{Ou3_+HQO6d3(+J zh8EPRMl=5(eUpcq3SJ(YvYbWNX$^Mc?#F-g*JdY=MmqNS=!5sQ{LpEQ&4!__tDgg6 zIPM(}-FLT)E=dkSDa0DHMsc%WMH|2P#)Uv$DuRaQJ!Tk3(Ik~6n9mCO*AZT=W`KwPo^z|=4{`F@cdG4t@eSg{ezJC9in~pnd#;)6~ zx?uK3_Lm*W_VBXcvG<9e=>i3wN8~lTzO9b>Lo; z7wonUq`coA>o4DDiu2e@_S|^!%#G*ov6=m4@5hGMegj?x)&OL96Cj0ip-!+AY6Bsi zPj(d9W-IIlP)kBUnxH6H3TA@CvIBR}r~u5;0|Uw)lK@=2V4t~W6;qb!#nF@-uDQyC z%uq9>cAueUv@K@mvBfrjnL(Spv_-)0ZNefLARv^&9sw{{1($c-4nAHE1-BCoUD|!N z2MscJc$+p%Vsbf8PK=$1PL0BzvEW8Qu%@U|``%B!-RW2e|d&5nq}*I>0Q z?Yy6N_UVCcDBEYIVd@RnU*D{}?pmW?DdA|4dGxd7i=W3r`>wQypke2Z!S^!-&p&YQ z9h|8wC1Ns?0UXPQ_rCLOp<@p$d!v`_<_}(cp=!g(9%P1*F>Nl}cgC^T?D^#@Km8#u z#6Itg1g_`y%_!UiRhrWe+d=o)o*M1(0(K*Tg*nk~_-ITMf((fUe8G){9YG0Rt%QfY2AP6o{$M2lNG z2{0AM`oyxt2Ci7JiV$47br!5oT9vuCw)AorwwMtRRwf0P2w5=Qnk`6%V0F|Qi~zB2 z+?G;F(oZegiU=)6$84#Orj}LAgS83MLrJSyKI8#G-PMt+RW`@+iPp&#d{XrZ5F?DE zIl3j~Jpx0h73i_Rvz)N%V}@gm46p)e1!fFg#HHCGg4q1dFDwHXplv~FH5Sl6a!d7Mv_2VO(xDxdC`(IC^PE(8IZ#IhSIr? z*>(F2Cd>mv;DjM)EdDV%cpkxxbvzZMP{u4#1*!yP4M{3!m=j|;K0A|9i)@yQ!Q`tE zZGl8OWsE7iypqJs&Mh)lSw-mQQUfn~h*6`%Fk5xhg0=b;TI1?VLfy3@f}aW++8T#W zc3KniIfA4@F&Tg$nGKS3FhvXO(MftvlVJpEDOD?#2MCLGOTv`kr;UCAhklljQEoDn zP^%!9_+m0bgcj5>6hTbui}|Wc#n1w0?n8FgV#bzwF!0_3Fvuxvw{VVG)) z`@5h27?zaXC9r8~BGc9_d3wNM*b_7+`5*uLUkTf|+>2HXx>zW?T1OtE*l z&D1R%40Zr`tF1S4JlJ_)Ak^959e3LP;6n~@68P{V4hB@6C0=Xo*RHzSt7x!K+B(yv zUDUPLS-s1ZEp)tW_Ejr6WBgi6AX~6wm#^`<)-hsytmxI(ST#-xZN&nW?1^b}mIlf} z&OK-D1{bfp-kQ!WZ?NII&NBm{&N-iS@^MnvpRo2Gd+rLT0pT5& z@13_fmF`W^$P8)hiD-2!;jpLuX}4vxf=dj*-sD$9(3RqTW;~L3*V;m5Rg|-AgqZ8 zob?i-(W*kmsBCI7RMn*APW)E^b5quj8{c}YBSrVP_9?vbHxe+ zwJQu3z1F&x#6pW+>t4%aOX3#WnAXNpaqtl>kFBxM7U}|k?6UPav6!u{q2rh1WgK|f zk!%1NOb#sDTz2U-UjTA=*)eQLxntNpu#6LYwg)Y^bQt>^&prV!fAR5Kzx4P`Pd#$= zA3S^8^8KdznAE~Kle`>zpBd{9c-eVuo68`x!`MD41uP%D&(@AxA3AUA1LtmX@IIR# zyziy}GQ2!*m$f>T?TKbzXZHN^KD$lXVas(UtTlQ~^b0f6N|3YJY80FtS!MKD=9M8b z$QLLBNYP-L7&?P1H`-tw2aIhZ+uRIO1DIenN%BA?NddIP0p$=tE^Poc+)9aDc8yI< zrcA-lbS4wecYw^K?JRrEmq(bDxgjbX05KV*d zgN7%pF&%yPj_%hTaj+e2_6RIwDg^D!xl`p2-goD%*Is@9?YE3xHk55E zL&vbQv)Mhi>=J5*m*HZs!WO^dt*4)S?DA8NKg7e!kg^?TvFDnd$A0kc>z{n&4tUwm zJJ7Kq1}_7);G|(<5W&ukuP`RaiHRnOa2dZJpoY{4;2~EF= zR8mYP(PQRxuN=P!bYpFWX{}YTnsQea+@l4HH%oE1W0EBilf@;1fV*?37Sq~{q&v0y zc!&>EakN?C%Yt7nVv@vC(@a1hiAl`Dz@42cmr5xWp(0%2{-SF6Qw4E2+@*jBAmbC} zjczD;43lRZ3>a>p^vKScg6xV`3r1Rg+;i_87TanksE#n85@MM>p2I)+isktPhgeSF zd23*+Q~G6BBfjlt1W6FI&>EN$e#l)+WtICT%h7_qfv%xdD&sY-dbQqGgdn>jc+V2r z>X-3MilcUlK#!VW>_;9hQVMtraR6(aIl%WKi1 z8#9clv{BV@cs+4+@o9Gbzgw6sVV^MA<2nMOk7TN=PaK z3e)G&Mhb0A(jr6Pu9Hf6>zq;E7hm7U9<>%!t$tObFhc{e*<}Lk zVGM)0s8&m%i~YRra@fkik6-HFdTFtV~4eZw#FamI;Zy4yXX}1b}pia zPqdL5)tc0rpu|eSRHFt3dDT%_>t5@BR&iq}9Ykud>pljVB0GHkr8I`fK_E>8h)Z){Vd^0K3MTt3kUi34_nACkJ3VVS{x* z>4l5-b->uJ?tY8sA8_CjmmbswU14pKTW+-}91Z+>L>H3XZu_m_aXYOrFVHQu$8`w< z>;jG_cHv^DlnE=50WxL=tW913z|p(S*a<*}ok=d*ZywallDE9oa_zKPyYFh(82Gi? zyyuQvwTSjI)ffVXs-xO%p?YReoBsM&zr4}<>h3$*NcH5hOVHSBzWN!*sBz{&%MOGA zrMY_!x+58Sg|4eVkm<>2W0Ud*i2k6l^oP!++bXLa4J;O~BNpV$BmJ)I3NtJ?d7|;)TgMNfuxk9LYF)Me_kT&{XqGwP=zCdy)q8hD`pEq6}%f7;3{cKfiC2XB6wq|aPJ>X&j zEu>qMlR=clqT13r$uhS5CX*`5>;WtT$Q`7wtKD z|GAsb-+AKFJva4=Z0E7zWuIVPyvHPO!1nO+Ve_|g9@_zIJIlh7nH%gsd!jdH`#|%u zxtn_5*LIt&x!xKhMArPa(u(kM`yf_VAnA#uFzp!_gGM&u*TW0F(W?<@Yzk7Gba1$!))4LAY=z zkZ3?F&Zf2)$i-BaBEVj$fFywIG_}}nEc=-U2+KtvQ0$U6hp~apQVBDRskWOP-nL_l z77I#Yyns+0K{cJ(#({#F;nJJL4m)_6cc97MMm#Zw*^lYnf-OJuMEu+e*Ik#_Pag z+sYq(|9f3t_qi+VDRX!4$F>XZ5{MRiMfT+%zqD6-c{4V!?B&>(yz^Y}*{1X7FaNM3 z*}l%~tzW*vtc&Wd5G5c9R06+2OCtqs1O-CD&ys?)XaN)*LY&TW6UHB8TzU@}NG@;V zR7@=aBeCF8YRZGOWfx}{F}a2S2>bcV_?3}<$|Wh30VHJs_9bCT!o-Q6VJ6Z-%bwxV z1lVL)>|SHB=f*+I^k_ab2b!y0O2TQ;=yC{DNit8G*wNy0SzEg4$bv%y8HCG+~aG z_KR@@)~dAyjEo3bEKUSj_(7^AP>U9qwzCyvkz@TsUJ@7Ov@BsHtLhsyv`e*9FUv3i z!2>Gdo~VhHlK1%E@=3eoIt1G^A0Mg$8V4a4N999GbidH3E@vUseU<>Gg$Dx=F6&Wp zDLvZ|vS=MG!n~Lq?xeJ)sUd1ClA4bOVmL9Y5rt}}jnvDM(mp1Wa0Gr(x*%0vH5Ih1 zxMc$(wS*O6af`!@VaTW&C8}S@y*yChkg$@={I5-MbT+COrWUHBBJ>EUb;+J;r_d@@ z$HNpD!#w?s26e0l;;0BE;@p+Ctd)gMNfgS$13XU^qphuh^(dD*s*zPE ztd(5~jG>T{QQg1h7m(3c+Qs#&p$U|w2u!vS7TCaAwI(BU8lOu^3aUfiYMB7T)I}*c zf?DV#@(Pj)_ykm{p+~v0^e!=GRmMaxQpTLqC51VkBpFF`;s{y7D%E7U9?LnZFK~ta zuUR?A$jUR?RJ$9?MqESNFfkCwYxg<^ucpBzZ4G<_)@7p!hDC#-;D%Oczt8V2B%24g z9Wa4we(}qnf=&!meVy5kp&xKGqBlhGCqe}!snOt&Ol6x*LmP;{`o~}V-~aqyfH1_= z5ojivTkQN|`a+O%xiFvwo71)rJ$#@3;3e4az4W5+CLjqL!i)ecNNBg0q%Bt%cHaEG zHrssiuxSfgw(9ddtXwWP-E3nAfamVD=ky)7ojqsw-Dd2VfJbO|n7#vg|7DBLIP=u= zFF0@M{tLI=ZmQ+NMHgQ*Ws^ztgU_~cCvG%RLD1-$YpuEcwC#b@)z?^kopslNg27e# zSGH<-)k@$qop9^NKK|QReCCsCD*NB{b7|Ku4!rx|sv<{vMq)=ME-@2vj&zx#=viSrLX|0$-H)mOa-3~eLWr68Bi zXEMp^xK=%2E?DH~i`T)XQS{TgYyYm7Hb=nT#6G&6fNipSpR;kjdkzR@qAYN1=| za{9UMjp?1|dM`0u6w%2seO2FxQw#PMW@hI(A0qz8tF|VjeQvlN)*t= zh_Y0Xk>AF~6oR!`$apr>Mi`%bxr{;0)NdCghXx|RV3I?iyj(KUpPp1PLlPRlz+hqg zCA#t6WayFro$@A3L;59AK7<5PRRzaeB2{=LeyJ~yE9>R4b-NXT)vwr6*`?L5wRAw( zr6e4wXd#&v)no)qYcegx#jT}r$_O0`f#u>j9nZ$G%iN>Q#UMs6N^%}s==605u-j0U z3W?ai`{EP#j8dO{;%m=5^4yd6e)XBVzwz9?&pdX`*S~bjAxpNKG3_;r_nxwN?&N)U zpRjb!CIY;C;67U|*kvPl*;h=to6vhNlL%-z`gu@BmN zvXj`}l5LCm=%w4*eTJ9!ow3QZE!Or+p*gIHafNs}+y^8AbYf^1UWPaEz068pN3y;7 z8T5lR;YwSpKp)f$y+WzBQ^^Y;VSB!IZ0-Mow7|0S$l7$lG;eQU z8B(^V?8RTsw0Mld<}wg{_2;heTImCpE)4%tE>NpzMbODGm>0*6FvtwIGU@(%Zii|i zVgdkiF|b^a1+12eV3@#od2KqYGRSYAT5;;da+=ZkLC2AG$Eee8TGv zIP&0y@4M(N&STqR{?td`;}K?GmGTJloi|?zFF*U#{eJ4{{qtc@NE601Mxa*%N*o}C zG-HZG#5m&vrZFfqtQuKCV2D*nfKD>;lNT!*CuYO5OR*#Z4ee~eF~*H(G1@!;?<B z0lIs-s|q0qH(bGjQ|$KY?kou{b}3hcyLq=`cXq2sOG+HVn8e)N(H8k>OLK*yC9#1e za=D4qDMnW)F?OtSK3MN88yye@f0 zqfmHec+^SamR!k*%Vc$0}=MG7M&z`XUH0yLF2Nj2$2HWaM?}&EW(%go~`D zC6Tm%vCJl*j#U=Ts}$yNrPP9%tsRG6;r{6EY?+~zyP;c(Sgp}dr@aG9MEX0ZNg;s1 zRI72R*7?~Fo~w1;$sUT`69IukrxkwvkgFM>3-J1rIPfWm9qvZkN4CdJz(aHPz9nf^mO8YocF}N_!K%h!m&E9#kf{u-Z&(t`WvK~GSb379s@106 zB74hG1zCsds3dh=e6*w%%!8OBbPtkFrOLu=F*7u%jzl3^M*S1&ggs@%A;vHrUa!{q zg=PCYHc>z#Xw#ii z5T-0GWdoOs>_2Kp9#&1ptGjxs9gZ#`)+_ACaUfSH7tr$>OA zW@tO15GZ^H{Cd9=ENuG~khDt+=i1waogqYUwbwwo3{Bg34RzX_g^`^`_N?x{^XBd_ zeOuaSKu`$A0@sRIykvpH$s{Lkw9X!T?sW3&k2&n{12&$#{`wQvn7-pS({|YEgCDx& z!gpVA=wZv=dj8p0eCFey{oG|Y-gNcFm%RO$V~-S|-QTgUyXrF_@JGE;=KS+OIgs`f zmwm*Hs;}y|`~%<%?hal@=Ho}|j_{1LZwm7vNxupBWx!Lqj(>|aoJ$SiacQk)g!-{U zw>O*p!>@j>W&KuQF7L7UR-G$U-N zQ)wW*PB+u3Z2KD2rZp|tT?3()YC8?8;S7g{wh?ON*0E)mDj0DM9E4G$uTrijqvh3a z>34~s+^E#~<)z0Q!hUw@!0IU0UG+K@M=)DAQ&TnK1a5RD3em9#xe$6X4c)Z~k8UVq zw{N{&Hq(m09rkTNlsEu>qGqZ798Ja(#c^yG557&J!o z6m!_Qc!12dn89HI(14MLU1qeStu~Z#1See|d*Bw38Rwf{dGg5z?|AIK8|^PY_vEd& zUiZN#?z`fsgLbmN9A4gM#`^os+*nw+$HoWkyS2|Qd)b%oFN+t=m^6Qv4dLbayG%G} zuT4F_>@fE7eKt8_(Ut<#3@^JpV9uud&)KB+WBdNH0XQ0+021vd!d`Do1;0_)JMj>)lAhq*;GlMq z!F^VcWDQe5<48)(3IvC?5vCa1XcoH{mqmTSE}jO;2xTLdCbbcd#WaNLBj>);Sut8fD zplK#-qrs}o!bz=}&3Nk*tBc}+uz6&=$EipaB&@Q6)isXn2y1UkXqRHEGQti?iX}NA z3CSXqW!)}20gF1y<+W^+SCS$GcETV&;`#HR{E&I-rJ5>hvqN3<2ao~B3~5=eE>@DX zv6hFKWZ7v+Nv9GiN7HH1Xt5Ns)-4rbmqdr50VbuELPrhx_JMou1bqJEU;jDh@RBtW zf3}VyV1-Z;Pg-vfu*pIpnhac^^VezF#{O0mx~m&~H+Q{M&(wmdCKu%zNG?pa>Agzv zI{+mv#mpmY^+iVBVTDkCkX=Pc!gqvqd)k0a3pPdn;-^2VyVj%hK)p`T12q%3^Ae_Z z*9o-LoBGz9ko zWtx~<=+PWFXywvRECW)!!`oh}jRik6cp4Ii1f_Ufy7Hcl*{yT+QU z%eD13Tia~4nMK?S~(^=hK&e^s1{bzxB53#J~Uim;dBXzx8K-{++-5p*Kta$=APOo^_+|;020K3eMvbm+ zI2c4Y#wJ{;MAL=r)Y71z>wfy!z4zVOkFGjr!<;M70&lb2-5gC!#W_SxF*#f{c|fhZ z(c^QDTIXu_AmeU@K^;vIykWc>k7yNFL6`i~y_ARY69n z6`{~1#Zq0et-xkMgW?lTL;9JN+Hz&XVAzl(bzq`XE{-XdaZo#66`bRDuf!+z zUka8O1^Cyp+2VN!E<2g2BeZyS8K-4-muTx}!E(C1IPw}mf8iI!7r}Dd#{*;@9laid z&p~OIPGyf?|26i^vR!6pt-<8Bm*eK&C)dx@89@4iv*``TsO4gP=M_XYGpqqbRrN5~QSa|zJFo-=pf*|a}=R;dq5 zfrMb7Gsey;hbv(~=+9+n(IpKKC8#)mpV>QXyS1;*dGWPfW)CtSe(-^Sq96VG7Bh5; z@P#RF`m&br4nrjZm{1!?3sE{>O(6^$Fb7&0i3WsWYFoxG5%>_ghm>6c!tk;rfmj%s z9%AU2Ff~FhmSeJ9S@J@@@GSc!DFrPFq~#FG3HO_T-wl{Yxzv;(c+z9e^uWb7k?lm| zFrG{O!aye|%`FJthLmkF3#tZ#A9K{9AT}j3qWv^slil;q;IrqQx7vKNU4A>xoU`UC zD@|U19jCoP<~}BM@2xi>d|t|ivIE#)GUBoie?Wkpfnr<9Hj$4%YS}?c=6>Lk^KCAB zKQ_FKfAh7UhL<0`|7LjE`3D`f-B=?6baf9gMjB;Zg2O>$F&r9bjRTnqpr}j0ZUE7g zloSLu;v{u0+8B(92rR!GFOz4C;oO)NqgiFASXm)rwATQ#M?s3lWCOfR5+}Tj$fSzM z!jS=E2P4gvX7%om=6@5ZSipC8aFcBzif(b}F3`HvOh`;BNgTHuF%Gq2!rg-ipvk0= zm?Fed5tJxeMyXwjUCM%xWclq5j&Ku4P>zn{;U+s!7Je2*Li?xgw$>yCH(9}rwbg~# zE!!Hy(gkgmB3NTstB_AD`EJ~9$L{RCq6Fac^oF2)S$DHZQU z^z@2H?ZlSlR^WbY^mZLj0y=ow zwVbYkSnIW``d|vR-bi!y8&C6j0+%Nrf6VEpoW!4A$;i*+ES_%TPo|LK&<7+r=1Wt-QMyKJ} zpmaeCYB(CKRAEfr0MmyR>hLakZ03wzylLmmH=N?Tv>MK_Pwnf?v06%#3DAL9k{9PX z1uE8jp5jyycO0ji^e8aavX+;0IgbJc_97W9Rw+3;f{ufY;K=ZrUa36h)gTh2Yp4f^#b9EN-c-~&KogdS0y+)~b2*2*|LljD0aDSKT#lmzJmMolCNb`={ zPZhkHn1zgCm0eN5&|za;-Gi~Zx(;vi)T!Y43-$&N1;fyx+@oH17@P|;0*p(RE+lN= z5Cb_OP&>B0uF5H5gtNi0szc7eCdldBuYFtlxwdy9R!qlw4L5tZU@Upt!8U(ESD*P% z$L;MUOxnrf6W5Kr^)vHw^BCv;AVVP&GEp zp0fvQnF>1twnnikm@y1+02%mIYna+hqLX-^)v?DNr6=eZKF16aI^oO-uRrAkAD=R< znZq&rq0iiFS~ly#r@`2#pLrAt{?@l0q4A)N!@%EkDg5ELzV_&2_W{xO-G96Isiz-# z`x-r~G*ri*5TZUfkF5r$}*3B>z zFOHi9BL#5S3@nSA5h7G>W_DX)b9g)^Ve__Mz=%<(bQJ{T*+Zw`en-+RhMkHILI3(+ zenpjAs%ghu1jd@3AQ4c}tmXDPIcNX1*dEAVmnK zeexzWkXaqoDCdY}%tdqxIIVV6ze1IQ#zaugLRJZi9&9VG7*oiBQsZQ*SQ3uBVtQ~C zCy>-qF6k#TfK~)yk_7P487wYI>sRqXrqYK?1!V&bn1cbbr=e5|IYAx8RjslHJuZcW z*@<7E)}=tR@Jf7Q|D|C43@`hhl-R=9iq~@5ucVg2h@dnXNn!vQ3KmO>WAWeeTO7Vd zv{pxq=^Q<_JnSG37`bPbL(SsR&hpp4JbG95*r%5JZWMZS;`*ykz|Otz3vKrqh<4c3 z%ZK5S?|$RiKYIR4y&oH1{@sT@|Fx%Y^!&2#NqOJbqS=$??7a5AyG{`1?KTl!hLpq0 zHk$1(JCD8ptSR2;Wq3kS@7;YhYq*`vpqci(8ij1BkNWn=IA+FCdyDdO!9;$_)!D`2q!M*S@NC-tjj&kAK z%!LZ!L2wcdT6?&6+g$dJFEQ+FjX}VA$;!kn)fbTM)js~n{lR3| z6INvmg|><5XQ2S(5&%vSQh{Gvz&4r{4cLOl5V3f`%Mi3v*@T%#kKJc9ZM_GK6ABP3 zZ_uw&eipDluKLbMJ2)+g4GJZx`fWyg#}`sen22btxGTXLi^D`EQ{Bs70~^d z;5P?(8Qd$fA!RgSFC@1a?ff>J?wMtuwX*N*m^vHg&YHEx$}3IXbh4Ly-NIwnT;(Zd zNZGcsk4cF=(QL;V;Z0wCh}qYpU}O&|do%Wu`Fp(O?32zp{a7coeU#bzzOKLO6F$Ct z*DarS9@~#OfBelaJ1PiZ0-Iuhw7~*t8Y5v_u>g=lskVIrp*Rhqk}zd=7a|6z4j|VU zG#;r9?aG1<_Lf~tz9LGJHx5m6RyFSOEOE!jB%l;ZV0p+C!1UOfMX#5yTC(~8hvlV}=T6hM1?cR^^mWT( z(~ggHczG!qOVeR<$D0FzCTsGE>#sw`R*v;5qOjV^qg@^!3UO(t#xi&``;EPrf!%NZtJK+|u4XjwL}uqGDQzMO{E3j8S`N)mH5XUZ!Vh{{3(iqF}`LRH{) z=E+5svbd$9c}!Wd>t=)*06CJ;C{$^k>@k+N*kofexkAx8x$5H>ygZ`_F}h~0(;EKV zyZzV~d6EH9HKGycjIr+R(pD@#K(&xB$jM!LzF;JB6fYTfF40^{jO)-ww1I=qv3iIh zBT28=WYbM>Oa-v2XLWP?T5GQ*wPA&$BSE!$?=u^c^*e*#n)GCLs+7LX6{>G2^@{rMMUpZ1D%T|J?jRn3*t5?)Y2*0!zV^juzUToH zKN#M7*WEY6&d+<9&b_xvee$V?zxXA^JqI|$%fKA~?jX8f1(jld)1_Z^)XRLXUTzKa zw|%c@rplsH1OQvKJ3{-#Z1CG7j=P2|fFV2>7<7qnA92$mtRS1k@VMG>5qG5rQ$hSg z16nF$oXr5qWrxstM%~%qcH@#-zas&CZj^4LZtftS`>&t8{8Z%@@0VEcNBg2SHg&X`a?s2(a72!_GKlpVId;(!@eF;rqz*#RL{9FlVS!`Hcz`wy^1UrLo zErb9)s|%*Oq#@@7;>0wNM?{Y=f)bi7Q4k4OR=I3(C6}@YSmX%I;B8_-Z2Zh{nK8l1 zp)8VUrL%_~F#$^0NxrhgfG@%nsH#r(Gj{$QYow;}J@BT5r*YmvL%=Z#ptJYa{cJ11= zYggKzQ+3V}#HHf2l}|lv)gn;}?Fj&4O1j42;Rb<~QZ>bs7Bf-8z(3TN&Vrh%1tKBT zpcqR=am6qQDPy7(gkBI&rOJISy6n#yWPjGc83u%V4Vlx+C+A~B#NzyC^Z_7;tbgqN zlrgeyTMAL&Ckw^%u6w7F3q27m!^Ht)L|e%NWJVMRmhZo34vg#z*#5lShrilTw%rVR zhscY4js{C~NRW4~$pReb@>|D?9tWZ5a} zEw|?6C6{^OXklIS`J+QS&x)5pKR6e*h3r6JSXIP+C^+f(vDcRUGT2Gr-evaTDRih1 zT9gZec>AX|Nr6-F8R!!s0W$T?=dGr^8hAtp#^vpYO`t2-2rWXU9Ri1EG5A|CC&WHW z5AeO$Y}do?hIgDjLidY#6g$y?wrA-AdfOtdwx57;5BcG4!P9$4oR;m3pxB|Xvj8c> z(GV_F3uP-59*2TyLMwKFRT1fCZ`UINDsjZw&|Ah3v`PWy_)}Y6w}R>jA}Y=%QbnW^ zC0FJZL1YRHY{E(qG{`T=%WmiyAE)i+o?yr;q^CpgG`zZ zedbA*UgXv6ubz9BPioWLTiBi_eC2CjJ^GlVK6BzRhfO>4m`lz(>FxXM_`Y}Tb@(UW zXJ`4?FCN+_zb-xRw5u+gcKsEX+;ZLJb7o$*aLz4uhd^C}VgRbfmys3T1V4?DhEfB^ zZNUH(22_iOgzzs;kx=u%5b~9`Bs|X=9(llg{^Ya7haUiPA?3OE-+RYnkIb8K^TpR) zd+zL6mtSzf(U)9u{G*TFJbSio^7X5)xXh@ZapN_vhB*)oR?|m-omG$~RGo9jEdmOa zMDDotrogp7ysIw;Gl^-z+wr&0yivrNhnudy%4!P{1FaTEoyvn~>!Y|y%p_@v#9+=e zTOwMNEx0W}W>J%*>AHo(tZt6=6jTI{>Cx^g$xWo_rdf+JY35IPt<)A8M4aY!>$??P zDeph%?MHn2!@`lD|K$18PPyXp^Ugi{q?5k#g)6^x{@2eq{%-JGDidfncW9Om^% z$I~0{-J3cLXU}sgB>{kTy%7tTz7uBy(*}XyG}T*oY2_d zRco5Ty#DV>4UZ6M;(LZGPFlvBCVq_WmP#p)g2%x3sXc0=uR3}8Dq=E}%LBCXlE%+h zS;)ebGVl~{S+I`=?g^fA*-E35q&;_ggL%?t^Pw(SN-(RuV&xT-PD78yyo$nB`|Z0o z3V|XUt-m%+{Q2E^6<;v$WdjVa-sn{_`s%*lzO&Y=)?Pi$H+%I449FUAM>~LkDl`NE z?XkllTt@<*Q)nBpfq-7UJS^4EJ zp1jhFR$Tst%P+UY3X_(6`HG_+!Yqn*wTY)j79GD8P|cNBT25JXz@unaMQCx54O&ra zP0t+K-K~lPH6Qwy_rmyfDx7+SE~+~8r(pt%vw$YhSJj0nq>`^9iV$IwnALZ^b=L6J zM{?A6umc_yM^R;gad2idS8`fPut~zu$0$_CIMTb^RxjnMxUW7+(gCuhh+E~VwFeq{ zHGn394w2ckWdXxMCj{9%m$2!)*Y3M?TtT^gAA@?zRSPC{?#nAbMzoGZC3Rt5AKCDS zM9;Aue(3qOs~u`k+SR7B4Qtbm8R_&>PO`gg1I~K7+v|67n87-0Id-tYt~54}!Qm8N zBt|qNTZj+&_(w>vcH1>J5cEcBL&}E6p`ZAeEpLrw8(MXMWpNC)arIKL)bDGt*{!y< ztz#Qp;>uzKZK}nMJ6BmWz0wtD7am+Ckb08L8%ClHaWGcr_w2&YgFHvt@2&gT#&v|l z3%LNMH*;<2+Uot%QJ=TP@8O>PeNXo6??b!Z!-c<`lw%)<5KPDdG^B!NW^=f$H>clC z-Q05U#Jmk^!9H-1r;c_%fgQ*Q$^-788{iJ`3GV<(C?m8u-s@?t%uN-yN^*2P1h!OJ zF5UJ_+lpxk&?uG?g|<3qAg3C@I*U;cS1mvyZY@ckWWs0^fK@?hwT8cdHC2N;Ax%lZ z7oJp=OA(efm9^ryRay12>Vbhh2PPJ5qy|I@5qoUw>)h})frJwU?sW*tvw}__D^BSU zwCam6QQ%tX%tA?u$tAD2Bmf*6=uN9+;*=_`BBZGTPNgIkQC%pY2*It4;m|6MS*lPj z*k38K3p51$0dtVtEy*$}P7oojjO5UeZ9_mJf`$+*&|xSPCK5t+IDuRN`1O>7Ks73$ zXnl3p69i3wK~eyXS3scb{09Lt3iojy_yggrt^>{*B6te2b0h>B>YVspbytBj{7S)t z7(YrN4kiAI5XYvo#f%5Pq#-+syx8zW6e-Apk_2j~lv^HAmBLe@7|;b`Q81*l?0O^r zD!Qw(CMqcl9-%78D2Nk+Ik72>D1wp@k!KA=VMqdogjWic8XBQvw^HdQh*$T7E4{j; zsyI=IJv2Z}6Io=8Q=BwxNpMZQ=y-$}vQIUA=2b?-p@|jb*3%M}CyGoI5?pbT$yMh4 zHvgE5+?s`W&iT@VCvO%)w{0Cjwyj)%lnZV_W+6b$X}|C8*`wPB=RwMS^($!HhrbZP zWE;vLv%Tivvt4HIFF!EC;+p^PfRBZZHR!hb+QXhWL2Sz@5Xu*mwf= znmI6bRG~tLkZpiMe@-Stg}h}eFHwD2i#HBYLVPk>thwUwwIT?V1Qnv#VNdS8`+*m!(GvC>U_=vFol!9QM)k z&aq2A!pmR#>d~O_*=L^Q_KZ`#AAS6^vz;*a5a4ry+jCC;@?{sEGX3IH-|?2I@7;gT zLq2-I=MVn~$m~eA_nP75YcIcG#g~8Z2A88?{Uc`$DVrXVcPV@>#qFOFMdKkt(?=Q(jR~K1XT@x1N@gi z`;k=hfX8V6`S(Agv@4CGbhYrJp2u$H2N7-w+zm&&C5L6af@hwwAO@MujZS3w7?C$d zMNELE){d(F_KBYhTnn5iQbqJz&wrq75#5+~$C=m7c)3R$`bplM^SX#SyknMje0Yrf zL;f%b3H9WTdT_+aa0p3k@LD)m`BQE`7Jb2Yxxfo!f6L123{0r-p+_A9d&L^SZh8q@lcO?X}lk zYb_KI!0Z8@5C~a_BrtwR#C*c}2Y5S#>B<_QF-MvLoMAgSfB+zj2oAsqwzq~oLH{>j zZRN>r)$yxY!!FoR5u1HWFR_?gS_LQRrWQ0=ddbDbl@4!qg%cr{EjQm}yKS}!%!01w zD5KBk1qd-AcuLUk-aJJ#`x%lLw8h02Tws#<^rt?x-r8$vV(%tfjGR?&jdSu%uo79D zytm7t*2i>915NL`b;k9k2t|7u^V?tj?B}+V#s{KI7e^g=q#lU#nk&C)_;^>y@8lf3 zsVidp)9-&JZdmEbn44S zj+Vv0{P8!2fEm&%;L(t0L{#OaJHK)B4Oa_@QcakE6`*aN^en|x<`;O@4nPd#^zZ~7 z!iB;-9C+t`Z+zQZciZcYyS#nh-QNAKy{GOtWxaJ*-e`l>1Tb0iSf18ibHzDIg6mryXdJ(`V=#W}On<5$=P|~pFssrdJdV+2fN`iYw zePFyNtN-Z3dbqrVfTg4nq9-eBi_JFH0X$&AAj!iVC{m9V(S`JRqks-5hL2vMdy_2a z>f{tpTXCISPpjj(S_POz@UG1P@7&h|5T#(Vl;}&=9X8*6mz^M4z3Y&JKf-{kPM)lP zvV)%~=%1cz`GzELy4UWzI^+yvZnM=^A3W$li<*Pk`k{x$P>h~tZ^eY;`I{EM^72bf zeOmd~zx=82```S+?ANx!-~RkNO|So8(8th!_v@dx@no{nPxK-}^qGJB^-sJkGcck~ zm8+&RW7d(BPvxxnVzx~;qsf&_e{^Wu- zqgHga@Gi5CjfW63Fiu**Xz+-nI7Jb0rW1-C`!Q%_Bacv7qxybtR4PPQN&= z>2|q77c#8 zRB>Lt*-4?mDU_;2c+*wshyW>ssIq`690$8e8X?5-JFO}LfXhyl;DSD`Li=NV**NJ?@gDxD~C z^ogm;bsL>+Sz9hdS42Uig0Lw|sDfR~m=4L2x9o!gWq;;1{Yu z4O%4@vD9uU&{Kkx#GHmwCkk<^T%mfUc?_}$;?+%u zah}C_6!`|03=}?aS5uJ52hrEChgX;f$c!SizZ?Uk3^F^F?f=U*mmy+juse?pCJT;X zL(1W0aj9_h{8`t}n|ZBp_naHGfMq_A@E}Uj~is7kg(JA_j+nVz;|)Iof3gmIu5X1+%Yyd65}j-e!~K*IjMN6<#`e zwm(MI$Cc$M%k5XbieW3Am*ZT_2E(2TF>oVEw=z%m~)AHebr6e06?A zdt|P5&7epULXp@|ToGL%0!SG~CJP+iWTQ3KTw{5ZHC9^=b_PrYsKj0#Fk*KPFfo<^ zWP{p>wi^f$fV9RhIv@ZnNQEW2{pc_SHZXy2kAe5@BLM`<@#>2c8nJ8${p{fmb;^M)0Dg z9U@yu#T6Kv4X(Xvy8p2Ig`6ga{xrI#)TTOs@#WnO zhYAF5cEA^0Q)>jQ3O-m7oIucpAXA7HU5_#rtQ!CJ!DpZ{3* zrcWLPob_9;H~lJIczksO16&E4Uv5iIo`%Kt*netl}f1^;nZ+T6U7-v zf+W}9_>|dW51J(eJUx11I#}BkY?tNlfBnN4E6k+fPk;Q#kAL{kw;x^b{qNoPv!6b8 z-#vHSbi+4qn|YNp4;P+)>d7a4@!|_lzyID@PyFJ0Kl#za_uhRwIsf}V{;E1=W*gsz zv&UIl#Dhw$W^sYpMSA~&-9Q!os>iC{2o^jVwe7{3S34VC`S3ae5aEFW_g>WqBte%j z>Pd63gthezrBK^p604iF$r|CSHtDVs>u+yEUD5vz}BVhyJwXp)FhKGkMYakAADu zCR`^M&p8yKhCT)_pl}q!3vL^CuHgt2dg8&XzsVSU&=qCTQNc_USK7MOGbyh`@)2c` ziio-FAS1YcZsL=*@rjyEa>^~Ta%s5d9A&QX1x*gCco`DlX^2t#_Y-L2mqdDY4i49Q%B~x@; z5jbl*sj93q)b8w%kww@_w`3@Bf!*|K*A#Qj4zA><%utqF@yco-6QwB$H!x8McP*ea zT6J|HhQ?;f&`pp;h>Ud!aXejQIj9kd>Q9DDq|ugSo&EoLAZ zkE?y>w@zKxE6jUsx5^v0U1i59D{s8cGAk{+=*Tbsr+AqU007z`u=)dWpczj9(YZk$ zGV}*BaCg0N1vYY=+fAOVB{3?Uc3tDTEIcAZMuC0^jT<4ZsZ&NC<3*=$}AcmgBao8uk>DRyeb`No~Z z4nBvJaRS$zYS4xUW$QS!MTzkV*KK2!L1@kT~xvDfQ?Q`ZapQy*{N6Na_r5)S3sD-!DkO8@W5iO z;^eSDXP#~8hMs)_&MbDt^a}*&*pck79QOraSrE6y+@3NV?e?%kKJ>ZIe8LIsFMs}H zr=0K^klFt72jBaaBMvzTUOxHw&-t0u`DdMY`Ne15aOK6f-FW5fTdtjZ>-8WrCyIy0 z#p3K0j0?efjcCYlw|F|@0=oD_ww8NW^peVsCkh8Z7;=fYH2{a=o))?PZXb~tO?0CP z{C-=~^Lf|8oLSdB^6;$39=+qC2WMV$&DW<-KV|ORZ(e@+$p$hB+`qKtE^1{B2OGj` z9)s8nax}3mRILfqX)giF!zO6@D8m&bgbn2yc9iU0+Qw?5Q%5ZV0t3Qe?C@Tmi2tbp zfFWXW=nvF&Y|^pADX)3W)>};RwH)9Y=>F8fA01)Fop#!C^U-&8pjjC)J8R)MqKtN* zagyomF>O%nJZ-zpCQ~+Bf2~)p1`AKwY+az(p;qCAONv7zy3e}>+jrtkiql|HP{F;f#;vw@v#!Rt-exz;2W^1;ru)P1xI##O-$&9$YfLD-K#|!f!dy>!1?- zL?=N3L)_}vg3d2h2L@7LG;m2Aerd@N*Q37^v-rM(@Hdc*>8ykf}3pb#+_ElGlL*U+PYyp}CVMpkGSl0}gu(yDFY zHe`f!a7%BiAstw|^;g8LqGe(sj?m+aCY!y+3&T{)Zo&xA3l+cQ3eA1fX`6o(d|ryA{{j(;JFp`L?5S0PXewW&(7IGLmg z^#S@zt#mw0DlVe;>c)D4R6)EXxoQshmk`erTvdS$J-(o%TDTI+bS%uIS{4JbW=W^;nP&iqLBC8qddA07W&cR)$#+^)!P;X9Hx6BUiU9sezQPr^&8g z5#`qC(-mb!@kj)PF?B6(ToOw@>yfPI5K@08->?$uT3{5Ys5qL$W3;kAHm26cGy%(N z3w_cs5zl8cPHQGtl);6Qi(~>_>!NgsNTz_EYlm*UYj?Le2@!BMEu;YStz21@LVsBl zgjo<#L0M?-ZCOwVC=+oCH%JSufNW3|7*#6pVv}(Mn(kQuB)Srw=8P}mR=MmzjnT|9 ze=hrv2L2d*Lv1!Cfh=$g8X^j^BQv;U?=pxqLU3ceOn_dbZ)hEjVzBExuG&k_F>6H; zH^9|J1?j*bu5{?_T75%rp_sZ;iDIg(3ZqiS-`z?pa8>o`Lk$|*mnHC-Nw3)J9{Rn{8bcCuDY|qu@!CERUZkau7w5fLL&gsQD^gxf28Xus`#Pu!;W0 z8k$1{MG!&H{-f3z%dZF}rdvv;a4LyQ#isX$6XjLVsK<_5A%Uef7PP*ABH*bX$|6t$ z5k-U?*XXW@k`n8-QV?SsO2IHORD@E7fRYgimpugtOmazNHUM2yiRKu@naFjJMuIF8 z10$%ND>ia)%7|g8$kQV9Dd4R1T1R%b$tOpU5zo`I54RKRLPB~X$~cHhT8g<2bP7hN%mo$RUkK8rv zFFTJt;9^_J05SwEh=iRnJhb2r-^adp-cA0${J>q;-aYr5vu`?Qv-OwWWSzyf-1x;d zmwg}G$G-ees{Lgc89MIJHFOLwgUSA|j3^>CyzEH!-rKMFj$Jo=+Z)z;%hWaAw8Lt< zY_rDZ8!o@f3X8vxzx&*HnR6?^ZeXBoP)HdD1R*_3w`asF_O2En=>a>q;nbNOC|h-) z9B?OB2RpgYTx&izen%EPwC|5dol#}_nvI;0fdlmpCN^sy+Q>JBieDc(}%|uh<0WhH1=YV2te(Vqyjql z3}xFwq5xpay@zC}q3FOn7z^`K1}K1x00Em0b{Xu2A;J-11Wy%Y1YjvIh=hxwX84*Y zY)+?l!dyno4C~!Mf0%;2?5}xfkl8&$5baSHSs;pFi~IFCFoo;Q8s!~5G` z{?|i4=sdPhevSUWeCCPYyzor#FWX|ax$Hc43$bLz^oIQ&^&Kv*{w}=x{qhJ=qpFND-o62<<`>}`bdGz7=9Pfu7nDy|3vllM7 z@xceYSA5}l(~g~S^F_1nm`=XU8tMuBLsu=LXF5#lge`ohS37=bgH_ySs6GiodJu#z zu6sk!9sxX>j`>=Cnvs({Nkk+dBw`hzG3RtMxsYII-B6s ztHUAsu#GnGRwvZQc<=01v-+w4{9-o^bTUlzb1t)Bn@`U)l*Azj-NUJMO$=LM1`41j zxKeX#eSOI^8=%vR`0e0;&ZB8WtOz_Yt`|3hqs|HE@0rYV)cKrJK^_ynIM;OKf{wcc zkJPO!Rue)v=a#U16+XX5XJ!DLN}-rQNul0YoRZY05@mFI3adChrBYH*yRy);uB(fR z3*;n|8m(Crvz7trz`h8@Xs#~&$&{oF^$Zg#@qUR4xDll+aYb8Jfa?+h zkfmI==$czlD2iKZlSU2+2x_MyDA+XtIui|nZUCm|2}EIF|1^PZA!w=c(ph$rC23spaWXQ=7$j8} z6~xJ6f{F{u%AWYeWf5YNmp~1t7x8oSF_m>&H$znAaGWaZliVsTUaz1eu?khR;FebU zzuGB?L9BFIRzVFhas+aeB5>i9g*AE(TO|VKl5`{)X=MzGC`J2H03D~s^v{-6C<|=~sdk8x+yAj^>kHoY@;nBugb$Z`9ib5vhn3jk#2G z@q#3B1e`JBDX+GUf^(t-3{fTu$1GHqD;}x!F~Xyx$e38#vdIv1o2nzVT(VGO5GW!$ z9vLx^Zz6IPCqyoiF-U#ZAaw{tkyA#@;@#3gK*8{=LSj}*5-_*6${qznHHs4vLlczZ zVoTK_UPia*>g%dff(RlxgVEd)o z8FP+F#yVwvblWP4BD5B_x|xM=TFRZchKnuYR(6oNePmY@w-$P#kA02c^Y}|&-d~1` z2e51(*-32Z*r&iGp~K6rPGvv5aMtXbuexXMjq`51{IUCPe)PWU=G}7MUawzg(^oBF ze|fvjmJ2WMyv57yFN4h9T;5@mjtGYiBvgtnN<>fDGfX z68!F%dXhybBMU4HAUl=qFg8F0FNc&x9L7fR1u#(h*f0MpPL!`6cR0N473Pn=e_xx+ zpZoL&{eRhCn2$f|(j~>VR1)H)M1q`@wk+Juvr&-@o_q z?=HOeo*6&-;XRK$e47t{EnINj4}UQFa+d+l>BY}+wP~X5bO6tD`ofP6tV0SSpt0R# zXOf+|cSb_%cqoKfz<@4`Q6SiuhMj?KA4i3VovF6T3nK%qAZ=NEt_o2Ae!W!dWl4Bh z5{gGa@raB*yEA?{mmJ^HIrgY8e)8iV2dM2G>$l!Y_MskhJCz>qcuC(MY6SmW^VJ>S zX!0E=5ij-oaMPNrukP10PQ3i|M?OtElpM$1om17mm|<|cX2pREhoW8aD@AriC|btWAU;MOvQLSlGIC z6i|dq#uqiobKY?B*|=~)(QeCbMc^)4Aaakb4oWQO&-!o>hVpa^%fN=>IvABqT%l%; zp+G}P>LPEQlPEw#9}~b0hi~CE2e1`KA9^a9@@{F;deo9vz4T<+6+sOa(@~X6rcpM6 zLXK{ZH=PN0izpQ;qDhOFElY8^+S#NMoyoDNv8t;$OSWDsOJH7Crb7()xTTvwG4>%u zaWdM4)Up7ST~W8nCA>)><1~tuE=ddGk+8I{CtW9T=p_0)=~PwJxc=iXu& zg^uUk;q8wRi2l*{?G%5jVM$da!O&)^k zYX!#FI{6_KV@y}l$WaS2^#nzrP*|h8bw$CTdx)zYn+Mx9jV6lQU}KajU6IEvs3@jSE;tP+7i^ z`ns~o=z-GsAVein)one^P*pW!oVM+WOQjDXvKwVOw(N$O2!yDsD4JE7*H{Z1>dkH` zkP9b*$Bd0x2cui|7e^HLP)nm2BiP*53F(O;qH5ns#Fv;e-x!FWvH)I5LJB%^r!&_Cr5pQnarp}1xclYI82IJDNej2`ejwk zflirBl(yo)viFV{>QHw-K}z+1PND-==Dm@&8#?lyjL3J{V$69uMN0^^}%4J3l1 zjufss49gU{fzDhv6W2G;*+*y;FeBq=i^RG@+y!z36p|}$C`)n7OR~H~5i)djVwHlT zP{cYp&U!e0bnRu%*FB88rU(%fNfKmC(ZR) z12zINkWelZ#igFOh7d%^M;!D)5oj*dSmdNbJOmDQrli~G73bQXsRBeeJsSs(Ws~maz4DF9w)Nw}L4c7C zzQW6}0^sS0X2{gmGqDhj9az8+*mRoOPbBTB*=V+p23+_ymJK6ss0Eqr6hqK(u{V^# zcz^=HkbRr7dFSR{gBjVAJlA<&bd|}_*IGuss+vM+rqr`_pE>^fM2jw73?HR zMS*Pv-)RARy}NCv($*xuNOl?90w$!R)a0N;Lmms*uv0IUQldI~+Q3}IVz9x@)Hatp zj}30i&dA`fvRr$ZKw3%UQ$`a&))4X8XF}yC+dBr0VP`weaIx^^FCKo}(f$X00=jR4 zIgbrW`#$!mCw}()vyQXBeAvMU!pp~f=`e3DpLon?pytz0I_jcxPPy(|7dwwV+FYJ9 zI-$os%TLZ{vsiPnICzpQ&_hzow^V`CB@B7-$ZJ3uVloOMjX^_9MyV5%8i^J6v@1u` z>P)gy9=O*b5a7ab);V|ijNZfZ7v6O<)ck|T@40jC^){Lx|K442A2*w`$}cspY2^V$ zXcz3&G&(SttY>{g8LIW&T?eE= zSFhQ6$m11eFWb8HC0OSfdawiTfxvn!9xn*!v7Yw$&D_Q3 z$=7N|b%<2(*&K5ncTtirO8xi${Vy2_;jh_@T{!=aTW-4gzPsl>{+$Q!SulI%&DZ?o zhu@Yo`}Uh8VffMaAGI|vsZ*EiSwpZAC!7}@&gC%c+Z<^v!k5-?S`D6RZe&~78NVdi zAuPF^qj2TFX*dwl1S>C?DXzEZIohIzaLdP&1QCefCnx-?UTV$n(qOJ!P%xQe9MEGH zzNoj75iJw?1G)(N2-YL1tt^ZqdwmAOXzds~oGPOK=$2}#Xn|HLg-D-3v96+~RCUF` zL`)$gid^-4xg?Q8tkpzKZRI$ItwfseOdLA3mT4)g55?#ppkNT_#^7)kSSrG3j;t$= z(ejNMjWOMVFw84L4W$bWu|}T}L=%9HG87Z&!j`c%Df6aAzP574FvS=oauArJv1vRL6KAyg3P$0V1-?X@v ztrHsW%1xgdku&vL z4Layi`Wt>%SulvBi_^+2X>Mf^5NV53k_d_>i>KjF?9hRw;v9yyB)6{Rr7*Gbq6nNr zO<##JqKxK&rvf$s9fJTp!B&8YPk#9hw!a&H^kX0NBlT0hcA~!~!N|@hgVK;PsO2-) zGRodzZ<5srrB0~&6xP{XscQI-w0dg)6gUzO4?<@^AEYKitkMM$sTx|N>z{%K(wzjY zjjo^Q0NM^iV^HT|Iyl0L<9uC4&{Cs$W(*LdsyIyqdX8$XmyS1-6$hQEyQ1pui> zCAR`Zk4<1__0rkw<7*d^t7h{F;fi3!EH480kxUL?3*nNZjv+xXlMyI@A+V`*{7Uh4 zHLoW_>zGmSxZ<~`Z)e|Lzb~!P5Ji@ZmSF>!BV0L#pNOEbLIsfw8$fnj(XOdv7n?wF zGRbkPh_Xz)l37TOtJKORs&2`!dV+?i+v^Xkl+9U;arBuwxe^1&T~oWODi763fl;|* zAV#3<+EfvG`=}kq&yf`8HZ&BEE4GMw0jFbRkPei_S4d5ue&jDjpU zQPfxICB^JID27hqL8~&Bw-O}@#42l0!)=8oYGPb20+pC1vQP{Kk9xU^3(6hj;}oKZ z$dXtQx3NhjQ(c%*97FVHUPoPe{9Gu7%P23RxG*Rn{xl+0>jU_+XAD&#E2Sab+!7@T zPq!%Ih01cR%6b>YlHi)aGIz{?9}U4G+Lr=4`v2VS??q|wK|{5pB7mAt|XFYmU^E8eu@I)Ve(oxui> z!^l4B<=l1eECb7k4rb%A*Zh{9)^Q$t*R59A@ii}-y5*`{Y`o&+NiTRohq0dnFN4W| zn5|6U&S#tjJDXr2hzCA+%`?0VEh0kD@Gn3I-h#d2&bazIioC!bEWGkcD>>~+pxsq( zj6%wGTFJ!kHz)+cEMZTW(Qd50X$1k5;HoPe?VCA}uh08++*^G8F)74jl?DPThDob0X^AOygFy12u zjRkdtroFK2yGE4h&1*;&ZuN-3ucqju)_g+Hm7#3AQybc40hDmAx3+!G6h8*QTyajb zdw1K0rcF$EnTygEv+Z@IlRzJ(E6!`nvcT+aZEb_qRAPL3;!%)&WMCO&2Al;4v+Xte zhm@1pPTo3(?X))fK?lD5qaS+Di1W0g>@T19^sy#oDGo5r23)uI`^#8S=(;Q1=FT|;j^E$Jp>Z+M)1?hrqW;k z^johknl$KUvo(FZCkyJfmDvlDfYQX3$joDAGeJq^f(jVin&pIMCrP|q&ZkKXLo2N5 zju(mtnl_ZmE_Swq{`)4l>u(oT1AG{CP;2$g6H{yTRY4I|W-exwgn<_-FoIMeThUm+V^LrwM4m~` zsufC{2d*2`!RoOZQN%((c{)(6u4rf3t=_KU;%X}1&ZgRlLrI7#YX?v+6%k|sD{MLa z?|=MFkEt$vX%VeMTai{Jah1{~D4+;|4tf_XqX{$$$u|8d!;)-;jp>xAEf9+!;$&5~ zYQdg(1k%`C3rf|07)15ke7C|fqxH81mrW#5gOwaxb(KY4MPwhs)iKJN` z2eLy{G_~c$LpPLAA%SqW?2M*odWTityK|1ucIgAgrbjDawO_Z75Hto6Xi3~d9%?9^ z4qA>xn3WVSRktt;wq6zIu@^q&Q800i7BmCN6Bb zcB2kI&PyK=S8PP-qJ|0i*eKZ;(PL3O8`PkRC?jUl1Qpc2LldVPItA*|Od&#B4V^Md z7ZfT<5kjLtgmEMhVjI*qu>oO!j9P5*(Qh~X(0R%hoBDNz;AM2d5q$q72%4ui0mCU7m#hb2|K{s4k0T2LbFMzMBsUDXa!rY#;QlD04=duk4k z1bI*%^XdaC?zS$+l1!~QeUE$vh5E}*raGc9ooXr$AZ1C&6a+?uscqlG%brMR2>kQ` zja^4jeQfq%GIS}sjDw+w&2$7x0;>giiItH8Y9VjH9J3&A?I=mr+>+4i+xU|_z*urH zyCp=qBABsdAF239RU1DpU|vQL^hO4E)x&i#Hqj@UMf^$RGRE~~rv{kWanCk9D%)S5}(Ac64c8j*S=mxToFi$_62e9spS>NKnFcs#I5X4vnTxp88HYIF|eTz z6ht95baFC+c$$~=v@0GAR5j9RB~?ZiXpFlZ#HMF5OC3t5d8S68t+*gB`kW@+>cuX&*ATx{!N8FrhsVZ(dvOHO~ zEHW)&*c=`kf`g7jaBQse*uiJVvjy?Kj~!~Z*DT_MHeyJ*9p~O(_A@EJVD>*L-~0OE zV+*{$>^%1DTh2fIgpa>*>RMZD^rCGyUv7sf%loC&cCTK}_rBh^-I{^}*#0Ng_p#eY zc6Ic6^eODEUm?KD7`9peB~Y_hnBThdD|gv)dHc(|Z@1QVTdcO)%l(37(SP#(GPnwe zfeauaKw~)6+B0=d7%H^ja-n@t*VlAy91{il`Bs>H zJuig9eUK?^;;TV0qodl2!;kJQY1t)(+5ImAM)uVsFBtn6v9Ailr;fb31%Yin`*((= z+$NpxfO&<}30S96p=pA=ylkr&R0?XXXL%@Wb6hHT%Wkr(l#|}TwcmoMmuL01-~nm@ z3hw!X>f?vRl}I2nvNKU`33n@EW197(Le`o_ki~5QQihHJVW0dueS8=j#o27z%7~I2 z#uh$%#KDJt@`GMsKI7ys*kAVl<$dF)1((}&nOQ{DJ z&bG@O7O)TtEy^4}qlOnJDfhKhOF0Isxm!!QG1Z#x7ST{D;@WU)7k!F(?PCo$uy`eA7utJ0h;AF8##4mBouV$ zkIMlj=ZKp0&`B%s@JX;*$(84iO_ro7j)a_|il!mc2^q?qrj6)nW&<}|hbChaY;JjC z;`v?T0wLNZL_}PsoxZzkPg6l&yvk zg&gwf;7S6m28swZNLF!r>K+CTHt}>r5B(vc=pkC4ZXP(8E~w`=-KwdKsmy}8C}PNnGq}3zIng1qHuOEi@x64< zY=>Ks9*?jmeNtNm=|BN`DzR!Jic?kT*y>j$F#;3SmqgS8&wu~-|A&W7EGV8enRYxU zDHnqnVpdF9%teYOd{n8iG07A%yW#}a)}vHVb@VRP$*omizMKYaN)fLIs=iiWXPPL> z^8r`Nlj*a2zF6XzjWyjdn>}`&>S&FKA2<8bf?p(i@~jUhhp)(Eq!QqaZG@o5Z@xGv<$xcUteFURevh=;B9k`7sc+D-D!Ya{ z$t2m2y3~UN$>9Dl;m5Mo)_2V|xaQ^UXLf_RS_T-}DaiNYyPKAr7N1G9Fb z{V&@W!~B;D17Q2>-~7UXa2eIkEgotxyDW$ms_9kVF>gVYWmL+5lru#NM0b^iG(ltp zd&dw@_4P49uM^nBa}2Aze#<8L1Ku$51TQ)4KrHjRiqpz-0}&-+P%j})gUixuERZQe zxDpdv6X?krSEHjVizeyNb<`r`UB&3w@VlauR>zPX&yaWE!AYiahoFMVDXA*s5foQa zSzHMuzk*x|Q44J16_F<5<&`}NuIWrefqFs$vw*Rt9}-B^paW?Mtm4YjQGIQ|sFat? zq0n>~DCp!=^uRM{6@>%^xei637toa@xTW^MQ!4)Q#zW^pPNF!Yik501OOmTB6K#^J z$zcHzau5X(x9p66C{d*B>O~r{L%GjX1_L>R!9A+AB$;YVT`fSC4{_Imc%da}v~~3W z-vbbE49sU1JeYOh%6BzCVH-g3hi`Pi5LN%{G*?_+y; zxx?51^023DTUqEw%#LK^vA>KB!<%JcLePMo?6NrB?Tuz|GH9ukPJp{A0>)IFLhTy2FYFi0FJ5XX&rUz^zV`sj-stp% z)4R;}j(uv_X>H6Em+A1M$U+od%7soiAgs!h zS6tDCJ2{9_xhpiO?j9XL<%p~VbjwbfQV6Gjh+Kr@@AKD?vYllxSp;5o7#m*p=Vb@5 z5uxUzj{LMQKG}4(xqSLpKks{AZ`pI(1Kz${-}{1>1z*X&_}s5ub=kCUoPXvm*IjMXrmCt6Gnk_l+s3_TlErehlnRHj%|>j zj)kOIr1T|^*||*3fus>Vaky>}JZOLY!-03JUEm1g zTIb~F^Y)D?&(J(Rf}rJPu1cW>Ay5D-g%?CBR|F@kDgrZ9!VjbX`l!(H3c>;^tENIQ zlb;}-9T;Kolkvxv!83~mLPTI0R>hoH+0FdbPSFZgDXJ=3P2FZL3Rh(vUL;lP60%bg zGjqu+F32v(tIsG}qqpAGf{Jccq6|&wLw~yA94%gBSW1Y}vriPaig0C|?9;76l|?JX z6&O<>!7YVdYQ|_7r9c;tv*vPa*Id@1sQ{`;);usp-2{qp}yf3v!FPf zA_@&)VvdJ;^#swefBFPC!B^ljT z!D?64#D^ypj8Kn46yZTZjv3R(&piwB>Om0*n%Na(Yq%y5(Y;4SJoOBLdIC+VJNnQT zRH`CizER`0InC|#>6duLW3*JgdIM{XBoZ{73i>jkgIJyt!$fSwN|2EvJkxWKmXjJEJln3OdjWm+MEhyJtd%UZpptWdg909s5{2#%gKnc=L( zZ8xikpy@p#)-Ft~UQX1wMa%+22M~es2qZSps$N0AfBeT^Js&veU*3~>f#rig@e%M4 zGvv!StONy9FC0n-n#hYGd@6!pD}<~o1<&vQ@N35d4n6D>B)?*{$roR8KH%BEq;WE@ z7IX!u6(kq=lnifS_gt0UQv$*%Mgc+5;wZ^)$kY%zo~AVPpl|<@^OJO`RvCo`z7#zKg$__i zq!BTU7i@@l(oO1+vAEJP>qSDj;zUPLZb4kPZ%~sURlvytA{9{;k}obvc$zd41gXlR zfK++Qt_T!aNNDQAhM}@tlZ>Z;qNAEdni8u;%ovjC7F|ZSF_1Zgp~{jb26SQ6)yFo$ z&+=3fAy9Elf#NpKLEsRH-Zc~97f>ieh=>wJ+@>uyGnimdS8F;;%3R`AUzS3*7@CKx z??h{hcUy^aRmZ2{Wz&%rWLS(^k}Q8iaFWHI@^C8K<3mU}3e+5$?iFUY*z6{c-b%iI zjtH;}DFe>VU3Wqolx}mm!`QA1XWi8Iv1eR+{;ZoXntSVo*IxFOSHAptQ#M>Wyu8H* zOFNIf)8;E~yUC=fQ$|0Hf{260;bj}jz_K0Z=(d<4Ww*{_i@$xBwf$1c_p$fber@Nm z*I#SWDwALQPrmonack?i0Fi-j7EPyry=-fV_r5KR2DL$#5T9q!UjLK@h~q^&#AV}{ zFAdE>g_119z@uPm)#kF}$6N&rAhUf^f1C2Us1IoSvaV8qP)D_u7$zmw@o&e9;AQ_x z?!`}kp7n_&-wifDz`|Z+wwvrj!CuI=xooAdxO*%4Glw1GoOXEGJIkK6Qoi7$Ox`y2 zCtUyEdF$SLI&Q5l`~>xdPhG)V{=ctb)5k6N&yn!BAe7nxbrqCEPhU$WmOF>CyT)2a>2iNyyDFulF+Gv0bQ+?3yA~B=z=YKlCdeY;1(wX(%dGp zOA(h zE;)PpMQ7Vzo`1(pP&3z;6U#m0-0}uP5QYiIHCTZzRj_0W29DIc;;wFo(PX#@C^++$ zpT;TGV9e_tvg02D$K>Jh>y0*-g<*dgy-+H;fPrF4C$KS|Z>%ElzK6R!VZMheIz8%QuV$FIzik0yC;3u}X&v3|=J; zs%8-S4E-m|tDWJ%O;Q|+;xI72QV@CgRHd*%O|lsP9`Rv18_no?CZeDN2qF|{K4c9l z71Xg7Ve@)UEkY%1bsN%b{~03?v3HLA7QxpKEP13c9SJylL||?8Sqw#MJ`|~BI?$rB z6(@opF}2mMj>MQMrH-o1o{X87#f+RE6<6y~xO0{rNqIenwdl}!pS^Z7ihJj1zkT<9 z-+SH(GGn8ghhRJ_E|%snvaGp^(4VuX{bdGZPq%utBU^5(4Rp4>30O+F$y za?}$SQkZ7zmBRT!50@Rj?5R1LY8*FyM|@NdlDGK(-8dfMbAb6#Y;a)yMP=qd+%nQEpt= zFVLF&A5Ea{>gz=z*WLX1y7_z3iU=V8U|`?h`7TYqh+cB!QoKk|#8 zMYlD~up*2Dt^25&Yq{8sLD&ZaL($DO5Dm;qBAK=_KJlxcxpgXlXi<1;-;=JxPM zME9oB5OnfkEFcn6d%4zcyT&iCAj=S4rEli@4NeGK+W_S%aKJ zNhP*a24pm;_2fY}ktC}N2~zRMGU!>PqPr3iO_GDZa@|UGE0x&hQo=>tB4S7l z{3&1Q^c0aQM3Dvmkdb;rr4ZwE9q5>a>RwsmX@y5wZVQMsR3l9W)#sD&yZ0~rqfi-?xSv(4)-2O5;3cPF| z8D0jKzyH`m=vdqf%kyr(wl8C&K+teB)a)wQU-tg;{qtvde|hfAOXuHl$&9N`-DI65 z;N_jSne03^yuAIUll+4DbsI0Y^A@8&FGI&RmpgzBBNtw`!7`vUfDB4Q(7&SUSh z<7#i&dCmSO^_mS=S$oYD{s~?NS0Q4!3?dVPu@+W;M0&v@i}+ns=n_Ko$tfTjV1y05 z`)QVjmk}Lo1Caez7(n)+BoGoaFz9t=6hD@-QbVTXIHA4TDl0*nP8o&MVN#`!w@pvB zzf9&TE3E+4o_F?{&RyG6wjB7*u$QVV2R`kE_@RG!FSeCeToF>XeeA!mK2U7$*fDJ9 zv3&sytoG)zFMHe1#o0Erh=25W3=7~>F9fQ-z^rn;m<$ovYDZxTx4a0R4E5{+x1SCO ztA!o&wO{#4UKU`>5t1Dc76&5wkT~CiDg2HWKidMoUS8$_!Wx_t1_6zkOIhr|+QTc% zn6ZJ$9dUO{6Ai(_^3p^S8-C+r_rAz|j4POIm-+m2Pd)9FW8Hd@8CV9HVQNRReedg- zqdxQXuYJ*Z?2FF*%HF$g4KF*7eb~Y8`^wQr_&&C;egVtgU%ukfX*XScS^r<=g7W1! zy4GcDcee&k6stC(kc)`Ob(D%3C8Arzk{W3Exsg_RULU%kh(wXkQ52#NR{{&qg_ptP zi6Ok~N)+J@Z4%t(!SYD$ka7!~1bDMtV&{aVa!za{;iQsVE(-UOs^nm%yi`0?RUI=- zPQFRT^wLDfuQTDG>!s!*bY2th!c~$hAEO}7iL$boLIpFkAdAo}ZK@>GtyNOM8C?Xi z+1OQ_oIruAcml+ z%(JHd`(OXS#vm9t`C69I@bl#A>GA5#U<{b<* zR3E*?h;h`5kgOzb;Z{wzh?=9;*P6Nz5sUcK0jQbWCl z>+lOm`GqisVOPq7LlPeiUvYYZCNCR>|8n3 zGz?5(pb5KW6BZE3utS8B=^Pue1l9r6PEbvy7k3q>K&TC;c6VE#28yOPl#DOUT3b^? zT%eVcoSf}ZLyZ4UJE%t=BZ$i_iJ=TDR@%<*&^B1dqgPAz-~RkNgH#br4Lek{&L+^1 zWTi;ePqcWAKGd|bR9|Emdd8#eQQ(oPTAAH?;J}8R0EdpzJsiWJ4+tLVs2aMiqCXVa zgt^qTmU0zGjH11)Zq`BHNicRG&;#DNugA{hfX}8Nob)G3S>on=U-mE&DZ*7xQ#Cdx zhp7b=)>ev+s>=DL%qYSGSN&ngGb597lk`9p*8?%g*wYM8ZmnM`Yvo;Sl~wlGb*lF) zEMa6Smr6#crql}lI-y-ilbvu)Bd*U7C@`|3?JzoKqgOL8NCh4imnw|rnWr3S5yP2Q zxqSI%uK8(IL?0koT?B=Slc0yF1&V7u8S@aRBsA7c1&Sb3pl9Bu5=CNjrADwZI)U(o zee-?ZyywQ7tPfS$g@nIs`H7r%`pG`_1tK{@j41s47e4{ApfF*q7;A#4zhVl8WaW{!(6}-zy+3yz?HBn20%#?tLyOES~qYth%}!sMCu+| z$|%qu3;|t2=Qk?7bhhcn8}G5p&OR1nSID%0!YV2G=`fZL=GimPH!@n=)z_ z=CyI`p~o^WU3!s47jdrMUi0sM|10w4(v*-XR4xljaGQn!Uf1fYETUWmsOVtem9lGME`eV$c_p&y#vy5(rM?`P=!*8Cr@Sw%<-v%KdKW7hN12!qK6MbcIr{4xv_yjp9Z6TqSb_k zkG%0$qi$W3DHk?bN_8vaFw9li*hGmfnbc6CE3xtl;*u!SEoK3+Qd}pFXgos^*rbw! zS)tfaXi`8Qlth;?ifaI~reOzpQv+La6fwx8ElxosO=2deTDz61WmQ)8G)Kf7|3sCf z5{@p5xR7o`rOM@sA}ML{40a}5L4`7=n|L7&lb;!fg3?xAk>m^&jK31grH+ER#SGJO zZ-x;}HEylQ=ml$20ljnA1702gaui_MA#I=M?Wnd?NEs;h+Oi)pgT?~9?C7;!=3Zfz zIx*N>hL;~&cxg|Mjn3ZR2&8*>r=I*IRe;5`Luo9N+s26+(R6U;#D+b?q;E zf!FlrX@483ARZvR$%gClXFy0kfK+(dySGl1ftdi~ij$VH1Y4I8L13H3K8wv6fKNeY zUXT-CUjDuQnr{Js2OYRSL}^nM29zC0gb;mw+Anzi<-h|VW$4&!sWL>YzV?n`TN~v>P2FJ|9{^WD=jiaLn*C@s z<@vA@inH8y(j8ufofYSa0lO)J5Ny7arf3va!k|z(FgSE^{&i0pmDEsh33IC+;eVumF=PsUhqVw4Ad)Ho{J?sPC zU-o6}V~+gP#nVo?@v2L%zv7~+zj=XQFwdEJ-NW~cHmWSj`OhLk{xi>w7w0y&Fs~0K zIv$jXL=*$fup-L3&pG6b@dVHlidl)_0c;hSoPiHpBvIex!smz5*C_WMGyx%P85=x$>kN7mz?{24fIX|0Y=lZv&x)I zc-+%$rzQDsjy3W;`I)kSkR$jbs2Ahc*0pI(smU}b;|#XdYPxR!gX^2&I<3i*p}-9l zm4z>dEt`|W<|K=_^7=3U4ovEilpQ?>ei&RTI}>7EP-V5Nj$Cn-GQ&qvQ&p~}tbvFD zr8Px_9b^=dMT@jRppYrAEEEgu)Pm}z=t>XJ(V>t>N;eLwD$tyUdWEiQT^M?TN+Dt{ zS9&ULSj$k9%cY?P^`J$_g3k%B=?vtIdhFQ3S&Tq|B}+mSVyF{VqKYs$^^}W3LprDe zia=Y$kyXHbbch0%pN22bVKSy)l`GfEJRL=L#KI8P4zG(FC zHlMt3yvKY<0mIh82K_CA)Gk$4M>V}LvRVXHjIx?Te-HuX8h|1!uSK|&l%!t0B8!o% zphmZdB-j>$wy7t;&g#$Rnd!h%0SdU9)7_hAsQixW<=QKks;3Dm>M)NLwSf40M@Z(~pp z%j1`-IShos4HGHk>vAUZ@r+gx@&%10Z%-G=(M$r0;xy~v0peG*A{2+NAQ@2_dImmG z%0j_euCm0Y7LcR3B+PO-*$CUNzQ!sxC4c+7Cm>>=DR2kBz)esRWMs3?d2IN_-qAr9z=LVgtpHQTPibBp5KXuWFM~(Sk;=o9P333t>8; zM>mL`-e^AEM(63#ioxio4Bkz9`SQ!!K5#IP!{hxm4w9o9O!W7^{1HskH`E%^XWrgQ zR-*IRKxi=8zBZM@YbZ9CZ9oH|e#Gp_04p_)v`90F212pGLDL*F6=!N~sxvSY7zpSE zMO9O!AjGLVZ@<#ZCrw)Zr7u}#DPJIGUP+)RPBzIJfn^y3kBpz)=p#s#Bxo0wq_DD} zV|e+TbI*jtg|*gR{p@qjkn!LD{%7UFzxe4RklE7-M7UeCB1%ORMCd@EruR%kDy_;G zUM5p&L^@-3#n8H=@3FS#c4Z>nqhaDL<|lsk6K03hG=!s%?BMBy&PE@Sm8Fj4h?Ac_ zG(=RQBA{d4jCmPZ-~0Wqe`!zo9s9o7u}KFkcbU3_w)oF~|BJq@L~pEX=tf-EfS%IR zto8)Yg*ok>bi%2**m4!$G zV-g}2aiGVal2s(#67ozvDUc4XLte}U4VtM5lsYITjv~ZXM5;)IQdLB1S(uu9N+zv} zh~pUo;t;~c7AEsg%a8@gItxcjD%EYyfUCbi3P1QB2EM_Qd(9oaw#y7J`|6huW&7Ky ztI+$)(6J3=?X*m9*; zue-SO*gI~qg7=s0FOOegUS*%DuL6f1!|vr}VAuna9i zzCedJivd93&MVBIoRh{jPn~Lp?>d$&JNyeC!pk1HTaGbs56t<>@@t;k|(1Gi$xu!os`@FV~fce!FWo#}Buq{<-0uDgXYUhdq7xzJLI9k>CJZ{yL zapyFgqNpZd?vi?{pD+>U+9-ociepS_Z}X-0L$yk59FR% z1M?(}9nPCLV)WcOE_$x4+k#s}@qBE|ylCq_=b1FZQOJo6XJKM?Nke!pyiCIXWEs!- z%|Gd{l>9&yp}Aqs=gjPyz+;C#U}dujCl8r!GGc~7-11$;Q3#`7&sLI7X@2>1@8{UG%E`>t{^s92$3Y@aJoguAV`%3XMUr2bSqISvKc!b z5O~2nyD*;aT2}rCFGC{0a(DzCHhJhiCk97Am~C0FR(7j`>RUr+QmJlP0FSpy)p~k;xPa(wZ%i zhEmF)V-TpSq0R7~A_8T6dxJepisumxGXKt7Z@%f;E3dff^658TdkxeKG7H~*)KNo6 zCb%)aIsEWfjvakX(J==9aTBpvK$_MFhyVZM-~XiB2+B%wYd7_ny%w*BsCyof4#42v zy)_J(T`Sa!w7Vu|H!Z6o!&qS0=vr1B=B>|&D3OI-C223#kg?vdLXc{Tal+hr>ZjT&;qU6oQ#kSfTl`E)r!^OSfu`c#_j{^tKwQ4I3P`5 zdI#wuMLJ5aib%C##e#{(UO}+}B1IGsR76F^1~w2y1f`>hib-rK-Y?yo=8Z9GqUkH~ z{{HV1&wUq`^{qAQtT}V`v^{%f_A`5CjglOZF3 z1#)5ZbR_nI3(pzgp3kROOPT5RFMs+7f1pLl@}z4kl_tO$Y_UX!X}(H1ut-uYq+SEi zz@;EQ89{&WGSDPPy)VO5mYQK?@=vIQ6EM=~tFB6Z5D+1T19rx)ojSPV#hooMl)lrb zUTq;K9D9_*YYGfx;fYq@hsZcN0`kwgTF~i(EgPXx5E4e?yfQ08$_&s@bEYS3S1c)L zDNlY4q^7Fr?k~VuBXT$Z;^CyHq*O{}RUbNbm)httBTqW{IJ@7kn0DE3-}@zCuI`X2 zo`b*`B8s>IMaPP;NWnPV!ylB1qWr;|+0uN=;+uix<}I3o0T2qP-^L$ z=)FoO1xSwKhDMRNB;`~3sklTmPAtiaE>ON=#U)dpQqH_0i<3^AC~2DTL?XiBWzrkJ z;-#^W@k@}CMH1vmh-8K2)idQuiy)AA${89((&bfOnzBnQfgFF_UIkW|WFcG>CGnc9 z#gcxwk&J|>f`vXCMN+S%EW0AFG=$V7C7fU=kFV%Mj6foB5>NRNh$3r4ih;$1ZYJUk zI~ivTI088#g{oj~vc;N-IWDJ|!Q_%1x%{%P+>#9}$HB|&%P_KS*qm+HFYkO7UKYZp z9LQYJ(2VBISGMoj`P}!uz5RvFk8N6a@0wK$RxY2}y+gI09jhHSuo=AEuWQ-Ro(=5B zwi$ck;ay@uw(pv!fW|zaap1GdvAF^-1IuR}*ZaIl1128Pt!Jlt9oyDx*QP;P?eZ(K z%a`Cv3vr*H?bYeb*ASoUodGw96?S7zWk@xF`s)5JV2zPf2!Ppo|0Xm6sEz&9&Sc6A zD9wrJ7AzaEK|*A)rHbx)E9p~`ZCXa`ncb~n((hBToBiN+D4&EFFX$%W|g+-8a>o(S2hbZ#C`tR zXPP@qUdAg%XTOV8U{wG=IVf25J(W;ld;*Z!nmHD5g;Z=f3&IkGUg6Fbfg&olim=6> zF!9Ln%D!&lR3im&xLb`>(*goLq*|2{Cs`bD$(?Cd8_rn;%gj& zLgg+3)4$B}CVi$2`X?HROIh5#q9nnjnNR7kUx}AL-!@}R00yb$+*{u*{m(`E91ka1 z9h|bt&|`0d+vh9$@|*0?n_(rDNj6WPgi9e~yFdhmE05*0rd^s& zGD@69wPauB@dTndfI4{D6FgBD4TLRe2!1uCRI8?z3diuD^GS0cQ*r;Wwt>X&X2`7} zg2HcH^$9@~Infg(fzpwhHA!|5+;%^Js}IP3wq%FTcuQSZHQFYQWft?w6?2*D)5>Dq zP;22rjQ?Sd`Ne22A_8~;Z-s~spVBVy%_m?%EFdR%`;`>6nRa zkl_GIe2Z|RS6SF9^fE2Vqf}nY1+s+Sl`3G(lQGT`pR!3x3gK;}R2d@hVSB_r{!C2z z5pD}duV4{ruV{p*6f`G?=+d%Kak(oM#}oPSOt_G^gh-(%fKo{_Q>660Dr!nAT=n_Q zo2r1vn4p!zLnU+sI7UpJoqCTzQ6&xz6Ga&L*{7dc_v9MS%^No`E|W%PWqTgDw_k^t zUcGhE!e)&cx=qfk;Yrf$YT%5n234b<1eu%hM3t3Crdq)iuWP7FdYmdNAE)2N21*ii zD=s}zfwix~tBV?`g475J$%H6HlBSG&#Q|(b=__-y5e$db$S1Ml@syerxTNXWHS@7K z#SsQ0|LDVqw`$SYUl)ZddHRLM(q!Vwf=3EL&|dKkjWVcRiWd=8Z1hr4K|;=H#A4{s zDYSdqS|B;Z7ILV{@=DpVh@pbkH1O8Nn-INR)D8`dj{~MWb0tfq|2ZZjjMc>?C0Tst za8h7GOvHpd!=2oH6^{Ao2vSHt^-Q}UNWml$jOo?*DV9$%qR}Cilt6w-*%AhnSq#^~ zQ%6Z9<)kTYluY_wv_28(f%HSK7-bOPka@)@h8q!qWge%!YVKRrQDql319$AL;1!(0 z2+IQK2C%hHJ>jG+i0Aq*ijzyTl|*?I+h3~;P1vHS>cWvUg~Saj2|BwFb#WUN929H_ z+yO_>7T6Uq2oQON2ce{3F%whzH8JCeq9)UJ&XMU^L_#G9gK*swF#yGy;LsT>vy*FT z#KdfGc7Su21KZPrYy-v+rL~(EB$O%GXjuSE294ojXMUNO1uBMG1Iv)IuguWe8q-un zTZHXA_=($IaYF;CFD78PhErLHGSCl8r3nl&0r%?N&1K`mh7ZEV6Hl5L11oML4o`yC zVOR_S8+ouu^x#>6()48iLH^R{ZH@2y_jpiR;Nvq|<|v2*0EfH_;KG+$;TpCiGr;ka6fG zr2wEA@e?j21>lz^=WC)Ah4Ct|a8gJ_;qiOrFA642PGB6GIDyI_FhhY?Sx8oWd4z`` z)%Ti`6j|`3B*^x1q9=tApiBslQe_M*sxL4u#mln@h)Xh&@yNL=0y{N!^p6DbEyd7&aMkCJ2KJ{h5aoZv2Td zU}7qFr8}_!WUFNyKa6YxHWRbg_$%(mhKudK4rJzmm%(KBdKGv%_GPa*#r*9TH?c4K z|FZqq!OM@WnBA{?o!FP1Up}~V&Hi0#4eVOSeP8}3#gq&eb3AadtFS%cWdNC3IhN&u z2H9y74q{@SII_L_zTA)9^}xmlv};tie)X@iFN2EUEszNFLTZ4sx!y00%<}Gn9(%;0 zP$3Wp!GUikS@&vs1)OOzY{!hM!J8J+nV6-<(12K=Nf|<6C$)agNXl|8eToP8$*u== z%2#%fpm5L**DF- zfukV#V-y;HSYU%@JoW^f=>RjtARN`+J- z;p}UtoHS906vVY?@9r{72jj<#tWmWpVmfwqs=MIDZf$TF0gS9jI3|Q88gP#7-WfO9 zVGC7I^+7o_%79&#EL;Kun9$g1p>m|jK+x>n_FY57ezMHD&Wl0WkEQOv_x7A&0 zW`hQzL1}(q`Q|y7FJC&x|D?`75$1>PU2@m0H{N>l^{ZFj>-_Ti zH7j4)`JYRS&D5sm^p~tG(bl+3;^`>}IDQkiPNXZPH+fB9`WIMXQng4<6%>+05F{?; zke-P~$_Y$rvX&b9ufoeF5u-syr{HB^Ie0mRPx_w9CLPU$^RC|278G)?)JR&QuXGP( zQwEAFk8mF4jA|WZghufYn+l^k8lwzEhB!kLrGiX)(kl#5G$o%5O^A7D5j+aa@pu); zACHg>V(KECi*n)>4ZmdND5K)F&|$;{Mi`OdWw@gvUJh`EmwAFpc#x?`IK!ik%FaU{ z3;NR>LbSMHGb5+wup#!OKF?Ul{#h;1>|Qo6iuMX$5h8tX*&2@o*MT9hEt zB07K|BzQ*@(1ZGix5jz3P}vOoQNW^+VvFjlJGCFL@Rk#H9Cj~IC!E*h9`Qr{;S2&C}-kI2SMaSATky0l6KuK~;OyVD|0m#7QwNlH3O!XtMc3{fgij;wO=3zQwdIm;8VtUa}n zwFpkQqy!RL@kIQ@#Rs{lrY9%CI}lucnUobaSl>dUWcDhtP1qp$}F^RYcn z(N#npJ@FRzc(6`_Y7_?IVM-phc#ENLn{N+st( zEOAv(cz&X5Z*wTs1&zjyi!dh(d_XLE!INl!5=ohn`GiI++|y5;Tt=g5g0n8Zn{> z!xK*ff_&jMU|b>&M8nIlo_)nY6L1vst7rCXy9SEcQ4-qM>FOAAHkToC0Acj~ekyD!oiKjlJybn!8l!o!g+lY-xtdgZsc?Rdb74 z{RXugHmVEK;)Wx}fw9Vi72qM0F&ESC>5wHraFutT1((PnMw7gw4x;sTS^O9-d3Y8{@W zcBC5HF`Oj30^OKaXTKWJa_uOOxSE3d1i@QA1Els(i1t{v92FDk8)vWL(OpC?_Ug1CbFrNI7?TT;a zw?18J=KON+j@A2gsKNE^RBKfKmT)mR%<&BG(SQqH_QU0&-Rk9UQ}%2#KF)^PSuAW+#o2)j8+x_F+4{fJ${;gPi5_ZpVhc{BBvYh8LJ9*&?yI=8YdF#14@Aq07Q`b* z_E~C@?bdc#n|hd-4aL*1p2p&AS2k$uRI)J#{x$yFtn5xO9(dVOs-wyPwOM1z#TTGq zadraBv;{;v=wa3{!6EP(@D9$i7uyakYgJZvh*-&iXs{hOKxP;L!m|Gy0n0Kl%UFOi z+oXA*SJhNzmT^C_WJIvGz%Z@iEDH?B(sXaw@yAY3B%E|1n626r7;zm1MS~{_fbEE- zgUkW&z;G67SepS9jXjD!Nv(Wsq4eu0rer&@p<{lIC&p#luz_eWnZ?-?%|rLxy5y#r z4=i7J-kHZAJ+A+CSDrQVnzI+pzx>YIW~{ht{@TZu+mF5C&c&-Ae&D4oTVL5;Qi-XH zQR#zNbr2pQ=0iGLlw>qKx{Q#hj{H$UbtRPgmk^$c`ktgH1A3yklmTgqo)q}wq$ZZQ z$mGi-1tGqQ#f3~RMR@4bwUfKVQ3}!1`Jr{}CC}s*W10IsX&@TZ^0=|WfCFKfcZ@K` zCBqbL$14rZ00j_bJTyibtc-r12n;W@IK<@8%!QI!!JLKO_~xsm5K9~k%$P~JGFYLJ zvZv9EN2b0?oX6Bm8K6XYMq4nAQZ1+umbfj9TPHW6^P6g^JqqUBfk97R3T&V@Z$fGc zqh1QBV2mlqsI@-*?zjI<|Hm2Z)rdF@=2))LFX+-a7MIHevgzA$N2)nZygC%5}i&i&42=JEiP#Xc624DnA1jLfgXSctxt$33PW9|7V1!% zNqOG;^)CP!1e%ZE(=7q+O>$eG9cVc_sj{lcykgLBm4SUJcy3`|$2X5)tQtyRMTm;g zo8h@}64a_A9X7Xb=rYW;R(!a>I!3WF{ zh{F=WPqhQ?NRdWY3x#BKPz$o53KV7%K6~bMI2Oo5ZYgytf@A2h&crH&!9qKud#+0+VxW!0iGHYfie&Ubj8PnZCYDl_M6n!L+D41BrXdEfo2-Fx3%?$b5`18cHl zD>OLAEGy+)>trzR+O1PSqUzh~j583EjEv!m2N@aaK``8d*#{5lPtU;({3i+c!)VAr zOpu}lMIJB#yVCE_ka;Y!m5!C!PF?5G{K&Ieqf#fL9eMI=KCOwXdOe5jqL>jdR-2Mc z?3E+&7Lo;u(%=cKeP<{4#&=(M>kW4gmi`p;*6U7clnzxHb}X2XgzcL*S^N5-yQB7$ zAS96p>f{FiaUNd2Y|2G#+V1c0GB%DKH_D8Ld%hA9W?wY9*BU<&2fs_l=z&pE(xtv@VGY8F1BQY%=@=&YPrJ>a5vkuZc)1Tm7Rsy5GowQ2Yw!o*x)CI ztGks4OC?GL7i&;i$d}W;@^zql?z~wH@kB`Zg+#!RDLhL+VIW^eIj_KY{T?sS`<=6H>TDDFVGBmYTR+smqs}3nTK~L`+i2y2uEP!L~?n zFHgvltnnm6%3kUC8YXy#KOyi`1+2gvPtI2~ipTjXUoM3duAI_WQ=F8h0xPyxF6B=# zkom-=UZUWP8P9%%;eW7@gBC$u zyDQz4>$VVBkLE@#L`=_NtEDb-Y@RtRP>yF*7tL1i{7UpTlp*HJftF=FK5! zpj&h!iGf9$ri|;ayPC2Mbpp%qDWnhHs;L4fhEjp9^a>%&yMVK*ZwrC(J$3LaOzKLCwL-ZU} z?kAYCkfWWAlH$j3>j@La9*&LGs#L)rSF14`vn=P1FUDnOmWAXAGBZ7U+O5s8IG1*7 zFPS%g)|GeOI{TZa9tkhcxccnrSDm$B-jv%GU48fMv!7VGZ1u|J_usYT$yE>e*Tt*; z+wi%eipEGk57q!Hcqr%eD;_F24IhDUeMdNdUen8vnNQJB;L#xP%JE23bO?p>HGaSW zS>Z(28H2uyK>3QBbmS$YA~lH!Pe%MHs;^Q20wg6Ps}|A2so%{SHzeNLlV}_E2 z!A(+9Ktpe#nPl+vwIW`oM47ui1x6(^nX*Bci0Pl^Wk<9q(#B5_jnxu~UTr*71%)vl z%9>u}wWJLE^hfqZkaI&Hyns>orUfyLBV9st5DQoCv1y~i-k^;6EM)ZA)Gx_nH zp#bX`??8TERSH|IS11F-r#ujulGx{f#`>56(8eCn8??bm^ooSl{6GyxefHTu;2NxO z{uG?FVhIO<`5S1cMr zgEvi@AQB0O`w;lGpT$XLKIxEQG(?x&O2OC6?|2@z5JkMgc`U*!U&$(r3^K#Z)J5Qe zB1$c!ThcIT)O1S{PXCMoN!Ya4vfE8J!&Q?5nGKJ2>_B;zA}uFkNJb38qeyaB7=4Xj zyAY2L{^vK)oL>n;soIhLj}L0}>vw-h)faO(%+U&V98qfnVca+^_Y4AIAaz9>NkRI5{KNYaj6#LC z|M<%%&KB8nsqR*QWg*azVpk_X-M3$_y7l(kzh#q0AA1NihLN{!d+z7I_z};WZ+&~q z){U?3*#$>G^5}z8FTc1~@2=Leq@&|F4MFVv-wNx@KGqCP%_@ik!i|u+D!+`>NNLp? zCzVPsPzJ9&7%R|p42Y2^HPVQ|eNfDTg*R5KUb#lieQsa67%tGzP=Kbljk#*IN?>V~ zeJZiTY6SxkTn3mEKis#UMB)WY)0E*SLBew_h^r(M_q-zJu#QVCW%gBhNK~MQCm9QT zGm3gOc=9MMnN+;T9$yJz_3qQ%zknN4T^7t^8};?fvnDf2-Z+0Y&;;)+y<>^Zy=~gI zfqXfL415Ne8A&CGo>WWF5G_HF$}31kr2U}ky6IQRg00h~&~kynr_Z=rvLFBP1HeiY zrSb~A-*ofbgSvLaCD)m%vfAl=?1_3GWyU2f6c{_M$pZ<;G_@x4(ZB&Et6`#a>wKX9 zZ-R$?d-w3q&2%4nn((VGzYNcboGz&~IrQYp^hByTeoo`+zrZynr%!$7we8<~Yxnox zei>?}?$U(c_|EPfTT3_Q8O9A1Mh|$|7B6@iYF4kTQ9ytAk3Tv%MClB;bpx!cT%{6z z0>A1G|AK^zZn^37$&*~U21ueeuNe*CR`R30z*SHhu_Mp;jH`Z%ebL3|S$U8}1{3M< zPdsj7u`9b{r}jr5Gv2B531dd(N?3Po;gUg21&tUM2_bzRqK>76F#DOcASXeK7z$~; zA}W4ar5H9FXrMcm-Rd$i1=DWQ zSS}W5fKrR}0+ou>BZTN3@$d^LHY(0f7z~ebLcZwoNfgnOf-H1TebXzy@j)z4ugK6R z5uU_K50#NvDTZ*aC}+hi1QP6sXb6&TvM#bn%4ArY$fS^tApPW$EQ)7Jg+@7xz`Z<4 z+*7j3!0~uhm^dj5;pD<4I)X$@8Kh6ZIC&*0^eBZSR#K2u(v*v@svsnp@_0>Pk$7IE zAbJM9V(3e(Jf(>y?c_2i5H7aSde^pfuyC&Va`Bg+FZ<6Fq|EvZDGOm@_LY6v z{n)Q;-|*7br?zf*_{Ghu*q2}1xoY&VHtfqI`ZpQSy>|EZRr+-<1DX4GspE=lNEsHM zFsvOc3?_T^95 zgwp7&E)9_1LN_~XZnXk7nNfgN9;q?lN*1m}W3v-fOR2NE&biaTeBms!wKcn}ITv|3 z*X+MhbYo;vZrif?v?-Segrhl^efi0hQyvUYzH+ogPcxC}1_Djikh`UIkT%6&HG`uS zhH*Lfeyd9kCI>7-SXco~D{2h!Y6>R!+m2WzQT=%MboM|*M6 z8O*{^0tF)jtFc|hCGNVzn`>Z|g5+5itJ+kNdHQgAV|GEmHvoa4+; zvq0u%uL7azd9$y%ZNV&f`SwLKPdjnUiN_AR@rFyTzxJGkH%`6frYr7PJoB-Kmcq*q zF28Nv|4N(e`#1Zwmee!3Q=%O|fiPtheJ zZej(SNJJD4LP>(2Xb|&ofi1MSB%*stP@oX$cqSd$c||WB(dEnWNLi-HD8N!cj7%b& z9gi9d8=+=$%easK);ffkYp5cYQ_QD8CA=J9OKpHDD0P7bhB8AC0x*rofa8;ai_Q?; z!=S_yjoJu?(nM?^MV1p}JO(I%l9DED8|1}L52TU$WcCbp@T7EEV-H?7HJg`d3oGtl z&gwyJGdq_cMtMer|Lkv0hW+&pwU8j>LjUoVp2;uJ?B=>WxI@J)_~425h28K_*P3Rjy%6RXign!Qio$LbSA(4GH+878FGp$y)z{D6N*51;~kC?LcS5Ua#cn33g&w!LZZ- zfRlEPJY-$e6edUc5(DA^G8+a-pp;Wz$U^#rN=5;XDo8~^z!4Z(s5Oy8iF@63 zZPwt1+0#Gz`2Ao0>Ss*MFYVs(`WtLpZ+!5F_jbR$^X3I}&pziHEAGF0_M91pJFMgM zYm={fkt-L87y#Y8p|`5C5P!e2Do%iLOuHKcGjJ)DVGy3u?(hXO7X6$cX$Th2#S7-s z@M-%n5flJ>+5`(BPB?Om5G@OFIIJp!gOXTmT`i)5stEea7hg5b!ie~i*6EgI-F7){LWj!SiMyivD4hD96=>!WRjm$sJkQcN{(GnGch3H+S+i>QZ=QaNWdT(Hu65hOsi{^@pAzU*2TAnwPCL-U|-7`s{=fO|2113~VDk&&iD7_}cYXbLT zY059DqIJTzB4SYmDRB8}s3ZVh6Fq54p(tU&z%a+$`bE4fFj_QE2ts4_1cy0Cl>z76 z{gqq4iV)#|Go)hYtvq9529P0T7}+aN%*_GIB^=$i zVefX84mq&ukX}vt9MrH=yZTKV*Zdm1Ok1FX(MUlvunyegm@@TEE98_ngAMow*-=-} zWFNJStO(4xUSmr3YRjd#*8kzbY(l2@nt~D3+IL?l(v?<@xk1yACU6Q!LhE2D%t#@3 zH5Pd8$4~5iEE=3)91x~FP!(twMt)neO$R1)Gc~1%{?n{+V@Ix76WF6o#I|tTiEWxt z9&^ovv18SO-v$;>WYDhq%3V2Ga^YXEtjWx3&;but_zG52M45XMU0e=curXDcSt!`b z=~uQ~Dz++M6hoUD`Gk_GNP@ZhE7o%h3)DXTdS%brF^n!?8>ID>M_cgE#I}2fA7Q$~cw(Z%A=g(L$=b9xqUw6XM zhfX^F&;|3RUOVlZH_f?p$pS{_S?tSeAHDC9`|sTFg!dPq;uSaH4ZOXyoFP*MeS@5T`uC z6U8f+h{_-@Df<*0BymYm2)#bGY8AaPtqFqh^z7D+D#Am*btt~j;!4o9{4_BSMZ#Fe znY?J@ByG<1(~Uyr0;3J}7>#X;CR|cVe_$7&Y($rkAnEnVkQ*gXxVQ#VgQpOQa8ee< z@;(Ivj-b?8zBY|UXEP~BouFp8Vssz!UAfYlmlm2_ zq1Lr?XX|F|+q4l*q#;3A4L;QbEOg@P7l5#}BPbj2kqG>F=&*rsA{wX%eHNx10Fej> zV{+i9Hl>n*E&0@;UF%x=szD{&0949hJu6_)v7=!QTzKu-wR1E>1BmI#)KF@&^4pW7 z0~e4G@*`i4-zR~fw2jbQAd+U%5S(UZ=7SM1lO&|2GV5t9S|ukOESly{Zi8EMa6tz(H$;ZD}FOq3JffEa_un}9}IFvZtvUw9Dl6~EnspII3ScLx! z&|1d;f=n(A>elstc>^XfHY^xJU>^=5)9z%S)&HHYAu2h(3DCQM<9#L>0}r;q7+4jqkl?SAa>E5 zNisMLEXSZsi>3h6t4TFEC(`+flB)T~4}Yh(GSizlEMKvQni>Z5?Td#BX&7g_S6MBn zr<=rS1#(mu&7`urn_WhIdUREC^bU1m0x?i>>#KS?(K4Kxzdrld-w1<63|zIiVWAQP zusT61vcN(@lmr{qLO;^4K{ir>IoO+FL6r#x0+l5UYV9@Jh^5{As+a6RI;-JAcnYb6 zI*HPn%OD_t+Nnk+LK)$zuaLfym(GpUDsvnO!|AB`-74v?Rl7!=y8G6sS*3TMZi|=P z3@rc8?|ua@Z`k$*-kE2e{DFJWggz<>nWh zG-aEl%t6-4foM{iVdRt(k5d>$!d}J3SOQ}Q7p@{uwNp<%S(-{^1y8HlP#QGBmfEyg z(;-6#1ZiGB>pB2o*zjR}`}MUYb<`1uk}LDFSsp*7NvN=nlM6?jG!|ZAEN1G43Sg)N z*xBgGwvjVV**Kq+vv6KxQ_g^CgEX73%aFk+B4@pk?D?d?pq8%(bv?*H3ehxc-i&1& z9G-Q)ow{`xmuv1@Q$z@e^g=qYo3p?+58H+fjbI&}mV>?TomJXmiiOWJ4c;wNC0=Qr(Ts3r?O&lSIiB39>b~D!S z&Slvj`ZJr53hW|so@fXX*=B7dKbB(HrXehBY}cw)eyOSZYg0zmG%FtB9#&(i(Grlv zYCPu*YvkMl1re13n9Q>L=Ibv4&YPdJ?{CGL#~<<=^K28+^*DEExNH+%{(;}ymhZvV z3Rw;Cs+s`isz)9)ZCRbbYD*~Wup~$IRr%mUdLDhukqr9(`uE=jVhG_eXh4qSil4>V zt3PG4O=CXJONzmo4Vr0Q>saML3E0|j#m@wcr64MNil6F^IgIQ?S68)9gJ_W`j8bbN zr}tSHhdmJk%hG2KPRp{48?`2%af(0!tnfgwtmUhC$}c0~ny*pgMnF0;0$|U8ugYT+ zf+Ye?Z{=kCgwghj1Hvs^wIFz7#vX1p0413uJd#o!ulD)tKGIQB{YBr`SYSRaqEY?& zb`JP!nEQMb)tJn(tT!1++-eK7!pr)L@wsqCa~rPX*GGIcDso0eujxbr(FhD8q-PsuBW7lHA3 za-mOB6<>?+c#7hoQQ#E&OX&r2#0HsIDS$*$1)hW?9W*>xfu5_#jiiGWzKWGsp0F|L z_^LUumrJ|E6AsHrtiAXkFsUUx+KXZcEL^fkbb)dy5-d%rpF?JFI9hrFqbTMnJdRlz zB4$wbG!=VwYQx6toLNqIOw7QtZQIbXE!j-Wwp%kF`~Na@3@o!LL&re0Pj+a7(!P4S z@9Xv5Tfe<)qx-&gKL7CgHMhO^{C#7Fx9!`lEa#W|buAm#tD#-i_G2G*NON!)E-vq{_o&QPIt5f-F@G@nUffb~M zkuSaIT=)@MbjDVH1meu9=1_ifs~yD*!Dv82inKtwhxwOQ%;e#fRmH|^fRwd}eVJ;< zy5!)qH0h0W5|gq`UkEtRQoeGhWJ-|zh2AI^O0V@&24y?0t#vz(Jbdr~xY%$Z3oEER zaE;wH#(6up=^LhC2pT;QZT@9=LTQk+7*5?aiSS1a@c#&F?1~|GJItxLvQ`WR8pVJY z7>HG0HEj(AQ~_jFU`(fCNt1Jw7*d9{CE_49s{|BG-=MMr!l1O&knyXL^#Oj2&7o<) znLbR}qYX-d#f?%8R;goVZEWXZ)oC}+f&U<@@VjW$Le>fgkp#)BmE9H1kkkSUC;n%z1b z&=x&4Cc{?-&!~k_x#KS>E#3YpNDLhEyK3~DA4gqCSi#f8Jm#w&fF1}M-hqtQjg#*8z}ms$;*(o&{8jAfg^Rb&Y zFCpac4WqDyLxVtJh%U7dlwfz`iBt=uMU!n^medR;xSDy#-W5Xu$uU+a zFwq)4a)hzMwibPsBTmXMTwJdtnNtgLuKe23(vXSFls9bnz}9V=Gcj}3s#iLF^2zV~ zm*4Sx_xVlh0cT*jRqJNWTQpkx)Z@-FmrghDdC6>UxFeZX15`8oft1ZDx-F^GqiugO zG4<(r5ZO=wD@9hNz$?O_;+SAqGZ6!8?8{Vig)!iqaq7t|Vp>z%`LV1^qyYzr;Iz|E z*4%*Vl3N!txuUWAbm*{ww#dR_m?I+mi-&NEp;d>)Kn^o?E>DalU6_e8WXa&^bQQEF zJiD@bTdp_Ly)W}l4YaR5#T9k%kP7=&Vn1~U=cn$1OfJvUba!))5zwl{>vy}XEnhf zQNG$xX9^>1lqKOfEFwmku(X=jsk zfoA*C^WXjUj&*BSIpMYR)|$o^RC-1 z6yP={Sa!gH?mfD|vk)}FU{Qrbam5 zmJvdPES|vBDyRxp2@u<*aLu*TY-7L^oPpe3%&w4{jI`52Z>8^ja~6q*H_^iy|pyD~5<9p3)}P7oT#MLbBjb zbP;jGpq*UA@=3Cs9)t!#5sP2AKs3n_bGV8r7llkYkrj!{I!;~!5g?9V8R9}Luy94J zA|0_Ln*zw%v_i-gELrC%I#0^LBd#{}ETvYy8?%7JJbQ&yM9uu5aW-HVyRwD+?8k50 zeQg_dtj}OFul|W4Fyy_``K(XlD?sjxr9w5vnT7bKv4|%P_|jdOp#wu38dJV_n8$ae#ls& z!SI-YSk+mO?RyMbS3)*!>Mlsjubk-Zbp)IlojJ80F?1+fGgXa#i$<~41)jx<;A=KJ z;ZhJ6Opv&6erQ)-DhrI$Lq*4A7Pl-iC;;mcdk4F=)dq?UdkW>yKcf*i@=S%J)JX@c z;RVpxrfbG!O9&i~SH|T4XBe5|&&KT1*M&E@@@x6hc}I^OeCmlK7v4xmIMrrsc-i^o zHILj?{tM>&HmzIhvag-bm428K$Qk%ax2Kc}0cqp+N{HEgXH@P($koCbHCTVg)Xs4~sVTK2cX3BSKF^DKo{MuAu|_(Mg0; zI5YByc;oEr&pqoKS-5uRPd3LI#3XLY7MG(mQWOop997WlN#!u%dQuyrb(mGnt#rR^ z+iS6Zi$Q}2+5JcrjE>D>C~Ip?R3p1A;2=t-)GRpb0#>9aoiLF~pbO}sN)NuVS2Cz! z7lz;xa7VgqnILc2q9;nQMbjo24li5b@v27sOv?lewLESRTcQ%=3RoJ)}AWPsfcinahB`__h}0X zq+}`T7ECy8gr#(N6CxUo3rBdhnOxTf@bz3Nm|SE8n=mD-zOfWQ14EdM;X4vog0!{} z?Aq?MxP>?c2AMzo;|D}nXZH#f8aeTSX%*-FnOCWdA;wP|Xf6BT5@>%&@7@MgmUkC7 zHEGzeSNCpeil0i3A=qL_bhgFv0(BZHQ4u4yR}x}8G&9;W3}6}(WeQU_Yt)Fw=;B1U zlI0=-X|E2j3_Ew~(7}^hZe^0tnrd+a2Y5`ivZKzKn(5fobx)kfB~>NQfQdb^6kvL< zWgoK5ghaNfnzUS~#48CGQo2O9&b}&THZgdzuxmqxcxxTjC35MU>l^V$d+FC0E>rM9 zl{FA_sdfi->g0rt;8&MdPpzx&@VuzI@7 zK^@@eHf>u#%Ilw9yY87aR$s{vLBdJB((A&TZW=Ohp#E-7w+v-|C%p=3YV))bXapnh z+E*8_erS2h9)57lDhgKXR+gPHR-hHad+xmxY=JC*6hMYG4XA+*;7VI(nRme!wlnww z2!#d6p(PhHI|1`FSmC6+v?C_#P`JuPoJbm2O?o|TCxhb<<2fx9&!datTcP`rw;D~$RO~u+%bZ+xHMF?KRg@w+bk75P!A+`ZOECBk z)n>adP{SUr?gX1K18#s1hNz|snIU2pMu_;6Pd_5|Y=yBu$_tv7CP(b?$F^j#Y{s_Q z(7Q(utzd162mwm!tE-ZV>Qzc>{`TpOJ7T2z5@~_-USkyK#3Eo;f|uD$2^j8Sspxv5 z)L0pG2Ohc*OQq>a-1J`!tvTpU`m*N6e{v2i6H!8rhJAUP%XxOJ4_@|%%0)NNwjbL- zYUE55c-a;AZ@=zO2QTBQE}xl8?R0JYgl8dHP!XJTn8?$rFWal#%j5(w^vuy??S&Y( z@eNCPw2=m?vQH%#IbhSu1J1M0!r-`PY{5xDn?c#JWh)O3HH&N5ku@f`@`AI+!_79< zjj_lS9~SAVRwqo;W)17Ej^wlLf~%nYoVnLaT;tlGU%f_kc5t6+)vl$}U3%Gt3Z|N# zu2py5Ss*L9Bg~*Q;Zb;NgH{QgbRIosG`#Hex=!Jfi$|tiIZes2h&4Qi1?1wFSj^Ds zB`HfcgdrO?Z0ay{So)xZILkl=3>uo!E*f1)bu4)5Ab?&cUIjwRu{AR{$I9%< z&zcM`dlrzH9T`lvC7azDSoU;y8FuEkdHb~&p5MK7z5BlQy!iC%FR$CS@!nmVR*V|b zs%!ho!}>NFc5p*Sn1NzuWq6r=IhJKlpqLXrq(^-}oQeaPnV5yMESHSSgIYPi%nj{P z+kWhkgF86CT(kPu;blyg|Gegj5n46a_n(c(L`>~H2Gw8IF4qr6?imRqhwjJIT*|#s6{^*0Z zKmGXqYgU!;virWaZ`epRjafE`)x-i*P#tv;A(SpoAb*f4Lky4JRN&=&1z=GEJ17$o z85&6mM_=?x9#~1p7xA(Xxl4o-O9~Q?%WK6D;l(R@%AtIvtmG20x6><+0=o>BNs*r# zsX&xcYPOk(#fR=;d!+HiKu2ve1W{9sB(^TVXrzc^`hh#ZPq5UqD+PuR3%^VNv`V%B zMk4YFqS8Fco`~ov#Tfbz|=^SNmHxg33{t=7|WQh@NC5NWIOZaYpI0Z3m4N2 zSve`y1P3NEVA7n`k%nx@6=>IlVbjwfiEp?{iqP1-L)nJuaF}TWD=VOowD4}?_^Llr zRM>6p2|=KQ6m|0*u%jUv1&QR z3>xp)x!E0Gmt1+&4*|W%u(Lkv+C{{oAJ`Utz85c`*@Si{;U|d#y zQ6vbvIRay2unJ;6(0BDUSHO3Y^(4<=8dI_p%HWTMAb?R^8Ntzv-EOJE9B>a$+;e3K zG(CbC$;I+1JQ3CTnq$DM8`#(&%_DgtWi1d($ol33qRL?N$HsUi;Af z%kEyb`2KtEu>MS7(mV3Y!q;D8bSs@THidbrHIL421;bK@)B29m zTm1&fLFQb*US*%sF#%7*mmX*I)*RZ={xPQ^tXx?R{p?@=AS7J2VQ2MHFCZ0qM1if+ zJM#1?&`I4<8oGtVA*kq*h!FIMIl~$!9QO*qmGCbTiDWon&g;-=(@Rn%R?;%ia0&?HSoE$Jkm--0N8;V3gJC4KaouOL&B z6i>?NX%SqM^8zdjF=oWk;R~=V-2K=NEEhYm*_wf3NEtfT$$SN$1DSKW*>-Dim^B#| z=JCow$3h@zelXb+i1vi6;br%Iy|Uw(*LJOAU*52Gsr}eTjq2FBO{Kwo>JRPHfPFdW zxcsuOmH=|h%>o&k?ahXl`*zyTlgsVd!rcj$uj2+aJO1$YBYM~C)3MgDerA2btoAI`oktNOHGCQg_Rjz0C|6OGawNb6KGS%^QH!K4cKD(V4-&0Ph8u;d(0~d-tx`?8z4e6PR;2~FXuP&k+e*r;W?8_2J3M5Gj?xQ{tG;Cf zx~{LHNP+!=DUF8aYs@8VZFF5*+2v(RC%mi-+;PW@rxsEXp?kzJMl17^Pdo?_JD6-| z6r^nT^-c3!GB(q~uUDVko_+VSMN1aV5i)DW74v3Z@$kL3xgYz`!QGD;Kj4Px=U;lk ziOX)CKKJ?y7tWjZ;601{Ps;z73%nfQ0M1YsDgQtXz0}MYNb8jYJkxg&Q1w9uiX|7s z>Z=YVkQg8kkxfmADEb#1AvIsUq70cdQ6_q_@brlT892EF;|Nc|^0kN(8d{$;_2`nK zTV7PjDux_|U@v8x+|rG;ku8WUp+*=$5`3VY(mH5>=brTqb5MpRV+c8zVbf)jk zH-`=y1cR7iDUddI(JrJw@vy=_WWWHHAmOHFsEy!Rr3YKcut^z#<9_BE8jit=qs)+G zjwgN!v-y;^K+T|QMeB$LLM5XcP&*y|vfN!(%TBf?rfK>u3~u&^6iE2R7hGVe*{f2~ zEp;M&!Me0^ns)UO0?FcO1588Md$y1ECBqHgw_IsP=yQSutZlF!KKa*8cGFPS^!Z;Q=TViZ0ohd%N-7E1G24N^N7vZuE^f~^3L0C zUo>>s0CwlubNuG@ryw&ZtqM9FHmVfA+L0n_+D1ZaS$08Wtj_v?Do6WD+YuNwwS=M= zRss!N@G|tFvKF72?~QfvrWOPN+>hi(!$u_5G>3elOXHOmcV@(x2GrQt#g?BlSQ1VtCiC*j~y z-FkJa>{Hp&WvrQyFY$(<94L^KP2Gfl73vyQm ztR8jPVMZH#CQj-|Hq0Y6tL{^^a_NvV6Mz%AxI{A5-O7eT2Mz1i5z!O?Sm7#t7=JjN z#5c{9)l^ql_=HzP+E)xWxMh69*kWRm0-h5{z3IiB&*lho2{`Y3ZpYU3kFLCL!Tec< zO1)B2n&8Fl*2=cM_OkMnc8p-Y?hNf_u@`6sd*Nag1POs$cW2=k7GW&Ys$~R_83cw9 zVO5|QmNeht3GUgnU?;Qim8%{ALM?RI8l9aCPK6_u2p(!>JDh=K+q_MnaHyT|EdF{8 zp|u&zLPXXabUmHXq;C?jLT;%L>j;$)Y*9#CvOO8K9Vk#8&1%%jY6e6PI9m&{eOmF< zQ7yFc{w?>n>%b=m5I+0tp9&y~GN{Nm&ph4wfNc!^3QhLR;P|8U2@Ii9eOn!@BZ2?K z=F>laY`LaW=L4$OsH&Q7jWmYPV>PQ445c-yC@n`x{^h$r3q~d?YGm9n`0E-3hT}2j zYal}6tJi!&rsAG}lD0!rIK+E7pAsv>f>>VhlN5>0@pH(MDKa!1PZ5$pQ8-x_Q7Wcf z6nL*3i*RZ3r#fmRYknyR7bPJ{P`LW0NWRKZzT%1^JjMRv(V$T|_d3Qw(*rdNTYaA6vc#L?takpfQ=$y3%e6T|((DVT#{Z zo3Vw&F4d7*pE4w+2fOTpCNATbPR?zxH!3PR32uuyb zI(28Z!$6rzoCTk5O!1?=Fm2G*Sf4ROB8*3L<`}Z&rBQ6@R0ZaMM_0Pjdpc;m>Y=-x zU!HxvwT_uC&9)6Y8?&=F8v%~Z9n#|&09cAI(ZNcWm1ozL~(2r(JlG)9e&C@Kc(F>;5W)`2+(k7q!v^g zL=eMb!cmIitLOz8^;Mt(pahqELXZ%j_(K^rk1ma-NI`;VB%WA4ArOMtGL9|53`x$? zQpoM1mj%d1F`!hi2$PKn9)L-6pobf*JQ;~78w_*AiJgw7N4hxbBAH|rLS`-#*ReiW zn*z+wPHp@llh(8ep7YV@#ga05!&H=QK!fHJxFLk*Y^tT0aXf|JtWDjbs#)%)8<<;7 z+-7iF)MNn&n-nNZOv$9(9BN83TGG0yruvaBe6&-623`WJc)2|Z$n~oxSW*c68GIwd zpe}rhlfZ~3!$8c0G|}K2hlbG^lj%z^8cD!gqlA>1=g2?QmUG9zxSj=}0CWZrd*Dl3 z_{#V2i)cfvQ375*=d8&ZMZW_Zbt}yzFC!1YsNpoXr~Mle(e-?yW2*>#PwN=r)m@`l z!N|~opTbQ6|Cte9+YJ{>il9aw$d!7La}_55o(Kf0sle!&G)Wq}>KSJ=3O$PINTsB> z3h~t^;fjG9io~&@Ln7K%UU-N<@SVLSJ}I`*MnxbOE#s5ytQN8kTWV7n1+eU(0D+1| zA*&X;kNpouCnKkY0*D(KBwjMAzO+L+R94GAj(!*`6hjF1rsVcf!;i*KyKx{66D4CY zzC}2#;gCR}5~FB|HUKMRP8o=9&b0)gcEpmbO2OU0SWr5@z+VZ4)va`59+47_D)2fcuMHV)qsGpKXVurWI zf$Y`CQjLXmswPv|6S}lSAa{C&c5!kOOwgBA&G=74P#!?Q=^<+iWXzDua8FQ)oQ`0L zMi;<&S$LKt;ld~{y4}SP2SnUWikx(N&l*p-$dKoQk?AKMT!&rvGUW-LK z0--Wgss*q{6C)eo`7Jl!NMi^2?eEgjl#m`sJnY2ASak#zfMPm7EHiiBEY>#)222Do zX>n#gKt>Y-9d5@0h;lfE6sa=C%FeioEzmKc;)&$ogNoZKY^mI`m44?5!C}58#(CV( z({+DINk>B{n-T(I@CX@$l`gsT0^45|18f0p0IP_f0^36i&H$>cer~RIP}GRZ<)SCJ zMwuZRNr^&+jlgP@1t0sg)e*k4ecA~EiA$M7CY=3L++bl@2RT|Uz#=#ibVB12Fo4Tf z=|}RRd#nQh^;vrl4C(5HGekulfj-=Zm57t=@h~!ZP-PV-S2QAnJOZYKzyJA9*usgc zS6Xn;aC(8!z;dFkN7^*3!!AP^eiCUzguzXVi>T=dBvxw?p10q8$>HT4+t$I$RAL)1 z{f>Uq#;0uJF0E%8RiAtIiAStkKDuLz-@ccw^3%I<1H&=RLV*gx%SRnOP6fkfR$yDu z@x)j5dMhe^+imfIz25$W^Dj7C(-P5N#fqOKNMu$%c z*m1`n4Vh|sEemm*E(w@QVVAR1SY#-31`h)b2&R4^oLwBG+I-D1^^yk@tCCzX?J}TS z3xfQ1k1Gbu%Z_Z{wXsW}DiHf?ue;Jh2h*_U#N&=Kju8NLN27hM0TO%ru>o);SAO$2 zyv$=E#a?}=JhEinfB~B9*}6|iY*QCYi)$LC)s#kWiiCxIwa}niO=}vW72B{wRMdjB z=vhV#QQ``mW&APPqu0bFM=X9{Nm}8GSUd=%hIJf|SAj`fM&fcjQjlO$Kp=#ZR|v%7 z=XfgmlW#%KBL#u~KQ2k-nOu^Ee5Dgtq~;UO7#XCjM}$A(A}A>Ng{wtM<%vKDE~&3@ zuM$r>VhKrjJ|$LCO9=WT;x*|cJeewz@<>f{Ap27HrF<4Bm!!-i5iW(7%a^*$N8ya< zX?F!E_$D>s$*vS`RQ&vhDbeB0yc94>3l9y4TFCKuEe|Y>2}TA+WX9#I zc5*pj+5Lh$Ha*F3%(xuL{LWjub8=Z=e#*?l1vs-f!^`f+etG*k`>~&S;*PBw?s0zk z!ZSw>IkEv(i}ejcS$Z)T{PYco`fEWDbO+*YUu!03hH7(t>p$ zUf?sW41R={X~pcYfUxVlVt1x6Gq%!-eHCbTBJ?LII25e5>vB;4zWmnhA!Qzv$w-b8 zR}3?FOi>xc*y8{YI=@NN1~8*sj#Qga1k(Ljle6a#y*=2#va@(@-DgmsX#_i4OH@8b zftaDcaoQw)l~QcA^Nb&`RUMF)2CFXos?2XG0QN^|JptPFRB~(b^ma8Bmqtgwg_q%3 zRaPA}1$fzo;SrWKyi#$yyVV!#n3dsZw_eLyrT_v;)@Q%mQU#_c`>|nj#28L3grNCl ztr)R@P`_8)bKCXPFZZJ;9vicHfMR%=-?3$8Wx&~1ZLd6_^pcxq-f_#E1#_=tU$!6n z!gG$Bdhw+BvoCQ)_Trnae)zt{c5AyI8(z+RU*JPYYbw7VDSgB%RbTJmp;P&kSG_4A zo+yKAeWlzY5W?}q`5Gq$8k#5^N{-T7k<4p8QS&&72;nIxrGp^l0Wst#1g1loWOfsa@bnZ{Hei4Gdgg$E?wAzw1veMI-$SnkRm7ylwcz^uo#h0 znyMgZN(lVxI4298q3^k8pS5+0?1|3G>CVz@z0SQoDA8Nuri5e$r;5-F=^F;LXgGON zv`n&`X_})paAsJl1wDisV42-yN>yOU;4$fE?3FUGOnn07I7M}$fmKM2;AJB-GI|1q z***~9vi~86=y$lP!!~Wys7BSw*rFR^3ucyV2j!QXb1&cGl2=r+Yt)^*Q|D{pvuK<}jVh?p%XPY*4YSyC& z27!d5bpeSW##=SDOo4Cej)w+$I;3V|K5&gZ{S9wT@jlTBr642udJ?>VFA~BLDun?k zIBcQ%aaac##gPq3NkpRymtD!=vV9AzeByWrr%AP@Xb{+5kS2H?yQqHYE}Ot2zv?CRUYFN1u=p z8&WMPI=Nq&KzX<=B260&N!AMZL)btVA;LMFA=q3A`?!SMU{byW#6z&@x8#{#9jT@? z0w%)Gq8NaS3lETJei zi_Eyu{|QtbAJNy(3YXtu6~Zh)67bn-p$du^T%h%h72qW7 z77mD}J6F)5yhjogiv;uW`Y?rC5Nm>MqbTJ3JFc~U=Pwk$Cd7VmzbjTYTyKNya z$d|ER$uGX-d{~dw57+^?BE3A|k8mjj1oa=#pBdc-V<^c2rgWt2cT{Dy%HTCdPsLE> zz;jWEH%2jB!d$iM)Rq*uBu(if<2SamnJNIahXvW(p=#=@UaFk+So4myFxDCUh#7f6 zLjiW_6FG0*qzU7KU#k-&JRxQgPJFY!h%}mju+jJ+u21+ZSh zFkwNOwq!de4kp8!TFQXnR5&0BrLw5v01?7);h4i-uW7(ZSl9}KG;QHtykr6F3DR1B zaNZo}F;Em+9b719Yd2N{t)$Kbs^1YaT`zY+nurZzx|{jkS`dD^{J|Cu9h?;X^^=dq zg3YxB^r|klWCOp9!VZ{2$`)I71feU85-OFo7yGp3OCIdLCJ&tQlOhqPT{#8v zwGv_xNtHP=i7jZPEV{eS)R9fu&U1?@)&yK4SHD61oNJhT$|-sZRYVo+nX%8uQ?VVj zw(Uea>aAK_jnb9)RfMjRT6+dw2_zLd2r_=1C4C37tLPBN8AVabG_O2f1&WJ4uQHXU zED|D+6B37>^C_>%rJ`R*k)1^1ijc4OBsI^9vAn8OoRUaE2#<1dUNJ|1@kCQGePjvb zNjQ(1B2Ml2Md45R6No0RNJ==eB9O;xGL=FS5s1FvRS|bDPf94e6rKlxz@lFA$|oLK zB>gWwRU!GJF~8{Go?2fzMO`F|M!^KqBFuX9H*+g>#na@QS5Kc1RHU&JJDagVV_UB| zz&Xd63%tzM{A&6Cm*Hh5W4o;3WoBg#SoX@e%*34gu{}A)W#;D6&h0(^a{JuwZBM)J z3tryx?D9=dEj|7CzV7>CUp`{+{v3nyQ6mmG=CDpj4(|XO2OWdPaiDZ;%3dL8F!{tW zUBPP4Q;+O*>i90_PwGEmXv=}!8;%*#rF*BQY|GVZlyAxYGW#+Z$WgZd#u!rBiz%*P zEJ)}HKf;XQEyTw#%1_Pa2M>8zLW7rqZsudUHyR=I7rU<+m#H7Q#|9o`Z)GlFW@V2C z7{O0YBC+tgM;c^iCZOgS5G;q9qnQTSh(TjYk)vL844+IW#yaEPnUhcTZi?~A%^}uT zt%M_>Y}f*4{HNGhU!_V3w;2fzf?-ukb!4iXDZEO7x_|)W8x*fp49XxaGJFW7uPnUG z5OiZI78@Cjm+8l<0@Vu&R(J50HUc%!_f#Joc21cZhGSG^_}WSWK1)^zF2Thv^HF9e zG-bs3Bte=pWmpQ3yTsuL`9gUQ+_%&&Yi4D?lww~lA>u`IpyRvlSQvoJvMk(|Y_GO$ zFIznKzGVv+&bw-G-%jkyS4};`eP1B6`YpX>hW{_KFF*OniftR7`R+@*P3p9C8dRV} zaHuC$UkF$P2zVk$Z}N%r%F#U0cv8B&=96cEg#%J(zk#tlD2o&dGM*xCJjf&~&7{C1 zBYuIBq5+DM@}wAI`6^bFK!Liv#>Hgo6|`j@yoFFWGd^%ZXE2r6z0Bg{s}0o|j+_GU zN+4N$Xc`6<^RLAjdaQ8jF0nNJ(2)Q3=^riWfNY3KDPc3d8Igz|G9mV|(lZ@G1Zk;2 zW;##PwAqjXBa#0&TW~+e%k+%^4bv-)i=Ude>X8TSPov#Ac1OFQHyRQpB0FWalbrG+ zaeLPXp)ncsX#!2QU>kLeCYV8~W zB#8N15OEeH^%o0AjCUj-!nGSgD@b3@5|MLZeoUy8_Oyrs#MrIZ>u6SoKu9~Z^$YE5 ztfrF5V*S&r!SfinWnoyL$Wh&m{8V=Ugl_5L0tGV;Sa^s2Ow$;Gy=DQDKn!FWc~fi! z%M{=f;UUrr3{3#kYD!0?ffRTdC$)}rWF(y9kum}vQw)i+FsOnG0KiH?M&PiFsyWpGZ;Jk**VP^z} zo%8}KE=dN*u!1nthwa`pKZ0P6^xBXkge03ic?b``u{2^2bz+_9bYQZi;_Mfe8KsZ6 zR84j+Dic8sS|q(%K_d8oQ3A0giwp|tQ%Sb}m$3VQ_PV(C1#SaWKu{srP$JR=6h%Qq zx*(v4ynw=5Yt8zd`Oj}=&z?PdX7+dX{N_|1ZKcw*7u@iKW!RZXr`U`}s7fL~ln-F- zM$$$#`o7-z1Xbx5^AF>kY{|qN2vY~`k}z&@W!2DL)4RO_Ey7P6pzeM&v}uUWGI$vn zHYcgEqfjUiyE!})h$_VGT~K&ymP*ZpI#KILPjfWQ$Rb`q67C=(Y7k`*7sRz!9u*Ot zMaL`wY4XnkedZ0b0f`A@Xls`yJ6;PfOP8XoU=-t@%oQYXs)K*(N4)5ZFJG|;Om5zy zv3s!Zy>AFm>?37J`IRXXfn^hde>LT~r^0dQLmM`(F>+~(FZXFURX42onhDTxbnA6? zBqUNT4X1AyP#5)}>P~?YslE^#Fz}&=gI+#4vFw^N%_oEd66#HO*;d~D4-5r%02y}| zL!}ScI|L|a-DAY!yKx1cp2}E#NA|%Tph#C6kxHTq4yyEwC;@5dvu!yb#N&qkL z0)Fg%JnV&t=YfAaGlLO}8(<&G~P+TkCUfWdJ}QFz7CQI?drEV)p@MpZz0 zNRb{)2Yjih_5N zeS{`LS1kdKURZP;%q60*Y{{I(3umoa^>zt0f8@NG zNy&0t+ITdf$xmqy?5%L$*H1nKjt&uf@Uo5|7Of*6=$Ip(#C&3G(||WwADICJBJ#hL zooeTLe@dRCRst6@}pq+LyG$tCgp=BJ-z9kZFn5$=3nNhCHB&a z`ToJQ%RU1&wy|IvnZj5>Y=)ch3{NK*$qoD@3Vv{VAhtgA7b5Vi$Zvl8?-U7M=In;1 z^(-yI5BmBY#0H}EJI!~d+CZRq8ZHSj zoHd!YIPE~ZrZ_X0j%GsG8}Z+kO&$K>GRUt(GilHSE%nnVF#l+v2|Pi%Al*;XyJ4$~ zlT(V!|1ij=lvgqfB7UZY9{H3(I5Isc zd8-OE6T*Lrh@*@dKU1ANGFL5yhpLHQg_;J66IX_Y>WbDC%!E!sEfH@;5GmRyQiOuf zg2-VJWu>UA5ON4=MG=WG{Vb>*8DW~H38`8J5zWCEQYffGT`4GH;;|I6F)XzSF_xqV zUKJrJxd9tw4qkR1+bki@BH&rJ;(1p6|H~g8+VPY3cY(>A$Z#=4oV&lIK+RHIi|y%D zcHY9vd$+CKv2p3{txHxfp2okt@}2R0yPezmykk3FP=|lHPv?gIGZcq1yxjka#tvQk zgxR;rps{qpY3;mk_j4;Gsk)TR~4LL3Z4-h_E36 zWKJSb(MdwSIQ~`!9zYFg%8a<`;AKx_Wm5{qG#fZX>hdJs7guCEivnc;&gL{1C^6ei zdgcng5{HXedZrG>o{N>7d9%>O;|#VqyJG!iNIOI5a4aY-1G@#2_}3^^vswXCty49F z-@XB6nC{>z4#9j)bL+LAUP-qn;FxH;w&%0krLf_7mt3}^rP@UZ=%CMt1q3vTPm^=m zz5zOlJN^e>Gp*&!2h=$fM>0MgWiZQ60{8*U`9b6)Oq_sqJlb_?l|F@Dyui25KK2~u za%`X&I);}8=$NB9|4b_PV|yhhygdJnDN`ms2QLr4z0ZhezCY~oAwFTAI_c^BKdG{1 zZ>?N3ck9~K{{OQ3vDu|D)LD$oCw7!&(HLR4GN_pA(1#T1f(*%QiMG5mXiIBorGvPL z%fphFnN=ppi2_mx#=`eX=Hv<4BD4HnCCCs8ek!XLHHvuVl~U>|nYdr$cW;vulQ|g+ zltIe;Xx+P9CZ{29F=Z8SlvsUys!6-}D4l0u-!d(z4gVl@qH<Oei5cla(UHs>cel#z|MT2*ZZuQ-rL-tO7Qbm73XR zd9^Mx9$22t42vv}iS?9edhBt>B8xFJO`EKGblCIEEs0VL6E?YaefjyPpuGd+;0Bi^ zB*NPZ5rAFf$#}MJVOPeAm9ERXz+Tiib9IUyQJim-+uCtr*=!{lBepKx%oa5ZmH}!UZ#E)G zRenzcnb?VrX-(|*-pvC(?qd*|A0QE+iCBo&ia;eO8>E0AJs$PIyq{4Iwjq{0br(I? zFM^I^rkX$m5kJY6kQ4Vaok+s)m)RhAnS)pZkur8j58M6^JE)6iyJ0Go?k!N}W@c_j z7lk=~Y_2f&4Rh_~ktYGpXxZ#G;D@4eUL(@4PY;_L*hYtZNFD+*y?9mam}5*$5gG;K zesUf=ddRf|-MF*MuA{clYT}~CnnOO8OO}|uARHcFts0DJ_0%ZqrdPU(7`ZcCt1g3J z5~f--7APkCh(#WYFM{$1?}+G~zpHd5UZ%-Z8tCu@6X-UW%$Q_SvJ$1xaB-L3@GR%L zW?!keHe9FZJ_B6q=!`8F0w*;4rjGaMT-d?(Y1Rd&ZJeNRh{*kKU|qjHeb@%Mi-0Ap zY2r@)L&}b1+kT_~fF#PQpz3sk>iQ|DS9j=5tGN(~Wv5hssB1qaYXcv_u-bh;>r>w8) z+p}5ob9-IcwbSL7a5wwcQ8Q*vgQFjR;*o0A9a25&#F{4#z4yEO4>+B&-P)mD)P|B< zuqcm6He)Sqz7Y4yNF)lgL1{9tJeXmo(h$hql?$}XwakD<<}o|idOtS2R_i-x1s;Tw z2H$muqo-V`Jb*B%4)qH5F^TaoxAHu8s>gsb9Z_)9hf>8+r#K7WR7AzUB`#xXgAIUW4Z`gX0QgE?9a!BKKxip~R2 zh+qH~1Gu(|Y$p1!+b%FM+LZ#OiIj2Zr!TChOr7M`YZT(4 zMxRna(1^q1x=*|Gn%!*lt;j$4510;?tm3Sjo|jfokG2X>ZxCp z5{!-nBiheQ??FtS$>y0*6qi|wpYj-4LWY|}ucRZTQnD39kgXb5V`>i(Z-o%A8b?#t zWJ(lQ3aJ!4@)-{E%-ggp1QRbRl`TA^()>)$N_8zYCk^owPXm*pNY%Q+xl)1*Ua7^i zs5d`TQ7EK%kskV93F7$~f}Y{|$R{G2pcXQTg#AqIq3l_-T6!2b@=4ESi+aeWo6;gp z_A|s~P%z9J)B(ZLh6>5d-Rq=j+%^z3HeRq*KwOHXE(3WBbnG3Mb}gw-3o4=MAi6NMwG z7?T&DtSRa8aNlsu5s0Q~i%Q`yaUXHpt7p?{IKMDik&kbU86+|T(VWY`f>(%hth11r zYO?XL=LkA87))Tiv1<9JNQ^+hWc0cG$V6Eu=zMD?43#~{<`OXJ{{Qk@vtD^^^2p0CX+G$dUhwkpVfVTs+rN}@Kla;mCi5>ZduR4W|3HK# zv2aB`w0U?M_)S`H_ z1q_!CC<$J9s;H!IU;ld`eaD86yO|BI6ZJK1@f5rzZL3ch1MvjIlQ0oM--6=5ErTR@ zrf+s6s1JR!P2sJ{FwOC_bw-#(N>M>z($hgiYA{w99IXAh9sEBL{Mp9X4Wren8ErEhd41P z?0~s1qNsw9DWTryacyVtF^ zxzf3AhmIFr*}L1j`~Axv|21iutG%X9o8+HKomA%pZ*RZjrvLTxpMLP+0Uw!aCXLck z%Ne!NE=#jL2MZYm^AnEdxDJ|D9dTh&gf8J6ho(f_+5_~8NTVdgO9y}*7y;gaOkpB; z+0+Cbz&m;w)_@N5wa#@S7nbqWwjDh30o*8|$q*ef1d2gp=~Rm-$dg!PAaHx;oTL&9 zYm>OdJ@qM>CtY_4KnIioi+NH!moLG#+{X^e@;~zbLN}BUSdIgfPVz|l=oktMF2=SN zN%y3;0{&6c1W$lkL3ep^ak@bwwqG*dZ2!NY`tWncTdWZ-q#S{56~|X*;i|exWdCKbtVo6B`|h z6ChPI5Ny2UTX;7WNXaOsjf78`NgiI-l}4^k&&WkwUz7QgMQ^-1bsW4rf8ML>%UqtY z!arYjB-`p>bRa_b2ls(gc3SrNe^Oec(>-BHf)cSCV9nxqwOaW<*UlchEE-PK>Jpy) z*+(?QPxo9|(QHA`A2Np=rRV=Al8jp}M~@i}ZbQ(d>a$V}MIiYAkl8H^mb)af1}SVvffE;(dOn)>V(I$)-M%}Bh)o;=jl+g_hD(mDt; z`i?J{Zh*s@hQ3#TaKHY&od(xHXS4AmU2zdw;mSC!aGYDU?HXa2`eEKt2kvG&HWbf& zUp&-4jy6U#-L&Fy5V3^bF@Gcd~NLlQovczUwSRmxZ!YQpeOn$5GmuulSUcf zI*v>j58ev0$t;A3^x{=ABqeV{Q+ko7$bT3eb)~pS)p&+2&S_dEPn6{h4@W-bPms-9 zwSrk%D+?IjpW^C@M`|Vc^K8S||xi#wgItq7$Vn=%)!mMdE)Cl`uJwx$2V< z7+c+%D=>2WtW4Zz_g%>M%XZV_Q1&cPGeBKH=F%t0`?uwCY#3Q2=dqo>=5dyi>%Tai z#ijV?%X_x0;a_$hd-Iyv`*ti?v*@KRmo&Jrd5z01tlQ<{dalU!{W6Dgzs?ZxxA~Wy z$1eQKV6qU%3{#8T*t;dL3^jY+dF@4a^ly1fpQcw`c6yhKPIo^xy!@?tHBPK`^f%#U zNDwIIJBEDWI4DmL0Ve@Bu+A$O5XO`UV0y|Km<)pel>$r$^9BereOWS&Wjbu^U(5i@ zoUn=qEF&J=&7mBiiijXxjTUs9W&p3_L*Ov04_G!!nCGpVyhv7)1aqC`06O;5Qo@^{ zpoPXH|Ih>Xom{(S!_!NyC|j~llp$r!wdnX7OGUxU5V7Wja}40hCzhbAR=~@?ru3mP z5J3VOD@7hn^PO@|_JX$@9DLAD_wmh1_ttIzsLNmGRH37oj93OOE1D#=js)EI#fyiU zdf{d0gTKRFMp$JS1Kr@cJp?8+pzU0us__X&^Qs(Q?HFZUe?4mW6VS0k*ZF5sx!}tQ zZFpGDW257f-jJnESrIgkCyglGAeZ@r=0u*dI)m!Es;9-lBz81tC#m*M4Ai|6q} zID)ouDSaew0 zRLDUHmZ1;irPC!R6Ld|3Q%|+XLL9WtHonj5fn{@}RhV9x@J%~R6jq1Kj1#_2co|5^ zB#e~#igKFhOiePQZ0<_Pk|D*k#y+u+Fbn<~9x zR(OIcc5EoQ3=lD`rWo=dqWkm$=ncvl>@tu5{1Yhff&a9AzH@P^w$2mT5s}X`?0+KI_qktB{3&l#}rK!Yd-R z5Mvchr~yI1L0j-ZC4l9?WGNy-V1-gB3#J!R)pwryg@%YTM|GvA&QgW7#{8*L2hjx) zc!U=i&w_K;<_b91VH>Z|cl;F_Skf;4N7zn+ujgD`tbvvu={(Cst>HvOKK3qbIFKqI z9||()j&z(I3C_XzuQ|BgtDv)hZh(tk-EzY~`JqVQk={}{+@>KEh8|iRrDq|g9=!Z! zQD3!Co2VY2Nuv<27nZ0D>7*yLJn+laiSnDTBnGVNR0GIQlPz5m+ePyJsF?0 zIz}~6!=}v!ktZjV|1x9rL?tv$8*#14bc|BbNjoPFNWglw6UnEvf*;!N+!51}CFw>o z&Cg;1N8~e45KMdVh9zp_hz`lPWzG`?vxF*ihbmO37t9q@QKdAbH1kw`>MF28=GXYXdw|-GbuN(uj^bmqUO-hlKHkz!6G@Ly4$iuoAhh{?h?-f?TSilqR zgNb~pV8V3!6)yK4Y4O zLwqU4o9saEan+9FgNCZyfhK>P@-!bm#)xCa?sm3zkbpfvGNE#sscFv4=mp>PcpAmD zMWUWEzZtOJ@;Oo!{X*x7PB+mpKM`@X$$>p++I8_M>n%Yk?!=Lf$d}`Vf)Qpa6`^v@ zV~-j2#KR8`89VyP1@mXFTK@K`NhN z$oT@A3{OAfTmFfdKFaE8yMP2VmM+sn@H5$YBxm|6wRZBoc1X#8oIC`6La>Z{S-;ytO_G8iD~e5=a^) zo(5X`m>+igt*r?`&Eg_n#RwPiQ=$)+VPs(0vp9*(kIb_inCuxZvyt%U>QsnfM`!gvN@OGW%qsUEnf~V@87j({jyj1moI2moqzd?i%x}?UE~E6 z-*Ba`l3T&fe)2f;Ci5@*PTA3Gu4F$eF?FM)IF$`D4;s+UWnVY@5%{NCYrB=HN|!Se{Uc^2nbjw8-v- zM0igeCuW2@i4A7PNecJzw~BMK0*``HU?U<3g4+1BAY|bGVv?FR*`{DUvRd zJ4I~?au!>xoy4;c063;Q5QcwQqQh_I6Dx~g=g{=XT?pv}dTS#lVOyLC;H`i51jz;$ z1FG2L3uFEAjI&fZceucKfwfLh9K7WbbA(b^m{%>#crIoK2+0Zs$5tgWVtwo^8&VFb zk`f7UJwqI@wzwLDmq`)@+3Yj8b`k|LgqKlpRGZK^vcU~jBE0$RZ+95h29=Ea=N7L-1X4dR3J2wxA;qZao1a$RgmPV4F_zTM<6&<^(LQlMTc$ zldvmHCa@&`k#E8DozFi_Rtz)8`>jyU!ub%=wWZGX(>I!Ckz&YMUKM1MSkz3R3=gWv zLT2bX?O?{1!5qZ^<}a{bLlcfks1fC2Vi!>s1)0rk7FmlmlwrYTOZaKYW`r=PEYN<6 zJofk_R_$Ck!+Hk~fda%CT4sJzpQ^$0-aWc9P4UBx#qMRp|K}e*2E@qOe8_kff$to^ zrni)hR=b}H)Fm0%fiPu&g$Z1Kz|d7oG!R~vrvRAxofzG-ZA*uQUDxcbQWEurBIHT8 z2M@YMMN(|YFwsl4)`1H9QI%JCRdYcx#-O3#BfQBYB{&z3HUw-;VUodP)N3-ytANBf z?%`$M_pu+;#dBvxupAJwprR+2Gr$3%fQquBfal0*qL$1WU}z!OjN2KA0>-lrK-5{!4!=G~*QIJ7+_;FkQQ zo1J*B^GPrCO=uJz%0tS(CpPWy(Q`4|ZPQ1@4&mBA;WF0mWMxN@VrvNIiOXAvPakN4 zc2RgK@G=dT*}ZV=F0XVsZB)=+gcIF9oD4N;W{r-fWeF}sM2E?Wd>HvL)FU@=03=~I z!plrtaXL(~p_OPjRhRDRf0FvpKyyfvWOzSSrJ0`EOENS~ux|tH`fz}%5CxnSIG{++ zuATL}mO7-Wu^7;bKv68=oMar6ur**-SBripv_cceUkVM$?16Y`B6_z%d$lVHV2Gvb zrN>C425M1P2DLi@Zk6}Dx;HR%S5rMi(vzk^-G3Qq?2L9Zl}vzo#G^w zx0--|uTc2aum6wRupx-E8a5a?>e(GTxk1;v?90Jy*tvI~?yk=E_IvjX_Me)6@~@?T zpPusOiL*P@2h%0*$<}^$?BChT1D|-$mG<$=# z1c@|#YRckDg9yw^P9t*&LWtb8fFZLVeBdc_;8{9s;^QgPAUayaNFzXX5ij}_jQUX| z(jZBa*L=)SS`yZLMI?F}df3BHj7D_K*hrZkK`QD5M)6w0CLk@e<;zo-M3Icsw)}=z z0@*lueX#5UXMRK9T!Tfh66N$3WQJ}4ULULa7#a4b$#mFMECUZ9Co*a@Hkz!x{Db_b zxwgyB51>r~I8h2GDuRg$9=Kcs^f(+@w*EesxK0ANF^LW{_g5|W0&=pzMd7Ior0bwo-LBGtvzmo^O<%|s&$|@I-FcREA52PxKKuNS zq^%>&Cwf^w!l#7HOKWii#$6i5Q423a=<21MluFYm37v7^(5Kww;}|o!<@})=r`eNc z2Io2`w$2JvrvoCYqZQEy&R-iEj`UM$-R&*r&04-tmX1lx>u!u`tOgPS6>>!wJ>mF< zslBBkFRd88y2&g} z6J*e>nnp3?*MsRwKjkq@1d(Wt%tf{gqI7prFk{LSks>6Kf~817M5(GFRd~uEC{;wN z#*?jTjg-ygQxrce5@xhZ5wD6yDV2iCQg{+bAQS1y>8W7iDK4HM9c4K+Gm*q)Q-zia zX=#c}^h!?8A{5l*42NDx^epHnv!6mCQjwzUVG$JxMb#onQ9LB)Ym1rp~Esk=>hpD7tQ6`5Jh6!^)X^%imeM zaOVyM5_$YeM@>@^b8N_ zGkkN$?9X?~8U;AGtP%NNXX9(&80vL*B03YGyegp3O*&MN{J3{=qR z?`GuJ_*XWs3@*sa@QFQdh$$-+nL%BFY3eCmPOrcUmau0KSq5XtMy&+nsuLvoDG&Id zxOg&%ppYWTfQ?Y|jL}=Q2!oL1K@YZ~zOdJ!lne|K_LynM)WP1c9{E?@><#k?gV6D< zoTy}zvlxB5XHGU>S)0r-TrFlN^9(c3EX~rZaqKa=iIq!5rK8N8kePnU!$gcJ7%0JL zQ3}D#^L`2}Pm3w5&zj5Fup)~vJmQsOW4Il~Kj6p(H7cXhSuq2PSV3rNj=2%Tj%t9+ zPG8`}y-(O6QUcVcI?ngSgr?(cCQx{w&((9_o{}AT>Si z6*-$xWspLujBtBF@uG6o`G?E&mS31m_(@SlkkQWJs28lQH`D& zq7{NJK$ZZBQQ>r-TG;c9L%1o{JN%LrnZF(c({SV0DE1XL31gc8v2BKNHGwUfdMBT3 z|1blDWK1;|v~JzKOBW3L208!ONP95@wh%S>7(_^!JT;k!e0*mUQdyM4s^iLF@(IS_({f3Sn&}cS`;h)^y@Q?Z*EQOCsSqIt zasFkVWfy8uB$=5CBu^SK&j>+;pd^wBB_|(NI{FezDqVR-f=V+)1qLCNW)Mne_c9JC zIc=xdzzYYH#+ofX?c5sRWT>;KD^b9Ddd*a92wO+Ih4l=?xye-|-aqLl)3LOMVl8)&V-%ao{ zq5!L6Kv`r}DjS#a+hnyg;*x|s2hEOf%lx#p*cO0*_U10U0NZK3ymy?`LXqsat)L8Ig=#atgWZ=xg zEkB*Ngc8{_^p_{y zP1hXFkP+;|>1n&bv|RZWZ9U)!9RxFVgKs|WCKkZl9oo+5ax9wa)dG=WmA~6*WLS@! zP3cCc`P?o6nea8Uc3iZc^Vl(cNkFS(`nnyCpHxA!J-s3{hbu_Qt`Qa8Mqe*D31mwnB7WAfZNQ zbUF6E-Og`2UQ+tUa2h3OIK-+i#DQ7g6l;Fz!nZT(bFw>`Z0u_|2uZ3$`o~W`_Wig# zZqL>WW(k6IeBE}3tqE6z1z-}If~F37CCH{eZ%#3dy;4J#pKB~m%2g%od7k-Ab{A`(+J#S!#V5UCpXHo+@JywU`XP39w? zDV`R2iyu5>6cmk>hhis~OSM83VO1Wr6qQB-eo9QMi?%C{j&ym@H&=TmTU843AJ*Q` zOht00U4Diu`4iHR;(26ClTvV)pIA)_B1sV;F+mmD_)T<7jQE!!0Uh~C#!u;)HYNI* zop#GX>~nS(&9*UZHnq?~8)asV~p3@?Avd2Ih!>Zc#>0iw5W zT()K1;vCQBbl$aTl_S{)cCCY#%a*>jYs9S!j$^E%sZ#p}<}rcv zRKyY1- z-080T+P-1s)^*F7+>9v5)WBgzdNLy!tE^UuA_1)u*;jH(PfnSWUy8Q|T`|4_oReNP zk8DNp49Qfdiqw*y$yVe2by#-uciFx z$8Cx@92Bsl6#7^aQM;h_aGUn3Ivy^_QQXr{8Pu*)Ae5`f_nW!93@H@Q=ZnOKDp;jFV5CXlN1|CNZOr zbewEyre}Hw4?_AyW(5(EBEN|OHIZa6JMoHQ0Gy`)<_9hP^bh|}Sw*DN2OVJxQ3{zW zWoY=2*6n{#8=gr(y*iQ0M!=6w#GAgrB#37^btr=Bn%^jiod<8f`szzF3HmD4ccw}R znlwOoLLe^MOJepn^rQT`Ri_}Rfw~}lp&{lvJAUZcDfNnmQy#U5>r+FEXT!p zEr0o|U+`wWvtTZycJL5eca5j-XPpY(yv1$6{y)@O-*)&3%`whlA0{7L2snbU0BR{# ze`dKwpY6&_wPdpi0rrT|lV<8=XWEG+!l7EEr*qA&5-~$72s2kBnq3J;7b_*d#u{J5 zsF`FUUBjt_i_s8A!6K!Bx|K}iG#os0*&Q~COF*3Rn8vMoCWp9abqeWuON$HvUFoN? zrVD}GNZSCkWI*LcpFx3Dx{YT=ROD?XXgv<+HlVC@hWMbia#&QG00X_TMmT*saK`Mg_#6#DWy>Pzj7G8 z$WkpvAk!2icM}C~zI`E1I-3~TUb9(Ym{`FP@f`|=wO!Aui_oMPN@_PF5mY3j_whi0 zx>a+`8_3f!ndymi9V*>HGBU)EQs!-Tg0Hls8%?eerdw#) zKluY6HLqSdf7XnbB>LCP^6%ZVamV&G@9y0k<<}hsg?14m(GijR9w`!~#j1}-e6*qy z@vn_pI^}l^59dUoQCdxU_84h9(GgC@j`avZr7|MW*TaO#CN#&AfCYuAmf=tFpaZ>c z=f|951IoD$Dq*Vm>TiGXX%%-fyy@#dY6Ih#qo|FDSxW?(Ff{=eRLDnIG{^T@PHn{# zO?B-^xSbDfwO0>~OXZOMJ+{*srIiB<7krM^PMVURFB0yX=#XZFXuN23;CS*WK zRH;OHqz4-kk^Dtseue@eh{&n2h{!L5rU^?NSp;z_kDPu+fPSLmm8bk(i3=irO2kQ6 zl7bG&3#!1FAWspB6r~bE5CwI4i)>Xs!y%TeFP>pQqEN6AW+I|IHBbhb3; zPz&C|o}Uua@bDJ0DTO!0S-`UuY*^!y#=!1pQ2>hLwSqsmVAsoD&R55qTuaVCZox zgUSE))A!s2=vm-pE@mJaQs!2MnmL_4{TJq+efaK=59~TrvF*LR+dtf2{=vK3-TDP1 zJCf~+?0&nh&xBT`| z&SPKM>CE1jH}Y?ndw2Num6x8|a6a*z& z`Yo)eNnzxG#yIg)&?xy~*kEnN<*}C8EJei9j6Q%YV9EL-Fd_JWSOQ+b^*nKwCoX2~ zLc~wo3m3L&l@E4c0a}1+8e)(z0WK3lpl#NM{oZc4slUS94oYqb0iHIkJ zf?=p^@m69{R^-T#vg|Y#j-}HA$~>^7O2=N7X{C&70a-yOvRGn>!pp|H%*^sMnSH{l z1Q^@3ZEO8yv)ZPmhv z#TfPwR6Az0-5@0p1;f0_q-C>GIyUr?@q!a3D6VhnF&flvdPR^UWt zbaWg$y!MGF0HhF*ZY3-7XL|Y8l>-+Y{fi(6C5u!+g2D`|Ove>qN87bmIu6CX(jz28 zj5NV3iNxZW)}=4JQWp*>jK+E;_-Rc+f5_lLP0l@+w4piJ)&UjGp$rhHbo>x+<_+@4 zInBvlfkP?0&n!vDkD1&Aiw(@!4N#q&*f7IDE9CF15ClmBd$<}mZY11vdB@TRxvDUq zs1ZwqZikZDC*zhN7 zXD~PlC0ghiGwcWJINQ=<4MXvTVLPV?YzGmeIn%TZ=hASPi_9gG6+I^?jim!L7KcD0 zkYcgWSSk(FXflJ@w4j+XOSGS0F>2GQ6*y_X0!^x``wVUR>GBG3i6pOxvijN+%%uO6 zmZ~5wBm)&9Oe#ufQHK2V`G6$`|Mp#-gsp$dNjRtNST$ty4&pE}d4e-`Dr`V0V*Wvp z#!>@2LQd0jF_Q+)U6g0C(h%*XIXaZuXfl9cg|+HwEcHeoldYOJr)icO3Zq?8uxI76 zkjZI$;jKga_Ld<;EMYRxIyDB!;Ee=`E^W8!nGy{vfCaiY!_Y|_YOfa2QV~U{7WT|Y zGDBx|YoCZEYNk2^+0hYsoTSnmA592cdo7zb_TT!Lc3kIN zrUGSAp!_Ctny-tQANpM)9<_xUkg$$0a4gm*)vRH-ne#EG?NJQ>)Lt72)_AuKN` z%25nv2HVCQ$Pbw_yjaouw(&1KGkMa;H)p@HeCeAY^M-Ycm3n~*G5)zl3uZ#f@G`Lc z!F#1SlZK>e1nVc!QI%{&n~~_eK}}-XZmP$Ux#-J3eL`1>?!J5Ok)M)h+Gk2xbbNwg z{?QkNCcm=esV@*GCAApEEIsOHz1DD+m}$*Wl9?94q*Q{k*)4JY5I~?67BFA#P(_Yf z^}6NetG|Bn>F<9BM*z9tl7jGvoWM1NYb(#8M;8g&?cvWgQ$gH3^5AgwV@jTV=#{tE zwv1U0455eqM-|-dJY?{)Lr}1`v)>%VuF*SCiDBVuA5gnN)OY<*upkFH%I{lS9<`RsZ?|NbUbd8mD6TFh$#KxAGGN=ifuQJ8zZ z)jB~$iacH=JwFR9=BGHyNf8lD;F@Ot6Nl2_GJ+CGU8w>=LO>r`DSlS1U0h~dd4)*E zX{e_xt2#2zUIF?@ zh3#;KNP^7XYDF57x8j*gO{a#Z+1oJmG}b(gYZ2*!h-F3KN6uwmIhYM21IRv#;!Vy+ z%_5L;4r80nfn`W}|E|sN?%Z^6&*noFTO7~kbau6u^VlC9+J0#7#^7a;dGC%@728(r z+PoZI-nL<0#kK|I8{ha~@4}m}Y1``Dquuw_`|`62yxhA(V|cmyMQ3-t@T?veH|%$L zldG?2*0( zzWDt5&CjV@yT(!f1TUL-IYz-lC^$Z17B8cM{}g)l%G3M<_kno9n~=51%@S;mVxL&D z6_;NI6qv+{Flj(n%dxV-xxk|genOPuC^PDiO?63^D6l0!W|pB^P*ABQ9(}}>Mb^xz zQf1lR79%UNN>M{rL<;TP3zUmmvJ|XOOfI7ju*Hkd;VPKw8uD=?WV8k zuTC8P?4!Bk%OUN2(d?~nna7QIWbyo|Z_aqJQ~M@259~4exrZmc@XWXokG=fjvlGTV z^~S3cmM?gNa~V=D-?++$%h;X4B15Ptbz}%lMZ%k$8QRKv1w&bOAqchyePH=8A;L4O z%o6W_8SKDpW*{l(t<%MJ56> zw5MiN4nJCiAgco>8iId61pM<6etB)Dx6-dGwfm;m|CY@V)dXen%$z0=J^}*aqQVB^;17vq(|}l5^#R%pasvuiOhn)X9B}3qeVGkC zlv(&1s0VSvK4cD&iH2yY)=7jJ5fR~cW7FnyX+qOpW|bhKMG-8rIQLdMK6M{ETp)8A z=&e^GIsy+ws;p+RiYW<6$|hZ6v`BMI3t)xzl9hfT&j39-q*Pm}RIeQ>TnSI5m1C9BbE#ek*CPY?k z85}}pA~HmEpRQMm zgkYK9;0DTs`PnBQ>kIPHI+~;JG8_nBFo6@7u^`xwwHD|R6i}u)#ZwVPq!StqAw4SL zjJ+y!3J^+YfU|Xhc2O8&H=ryxKDVJW6ay#L4nsz9GFOX@5pB_-bl#wlu9=jOPU=K$ z1j8d7^Ai2BcB>t6r?aG6Svc}&g}0_J!8~Bz$tZwHoo zXRzyEwgg$3?o3nW!)*Rb1ON>{@-*bFDP(0avJZx`=}UEHFyo81C`*P!O}1tm#AP6J z)S{*O7xpw!XgjjzM$?PFrbSMhV3 z1a{(D1Sp>I`UIf3eA{Xnz-oCw=8?l6eR13{nZ;#*k>#8;arl(Uqap6~YZv+N%<%;7x&bb@cb!ZCM!*x`nu?mZY_s^tg&!HD<)=1X!pm;anLP1DnhbR7Ghmia z$|qfp(10y)w@TD)%YS%Okvu%e{v8+w+^Lu^I1q8Z;%{I6$t!wm zIEe5zLi9H75tq{-0=?bi;lmBk+w};4_}y>i)Y2$C0E>P=kX8udRYO3QXY$}Ad89Cj z#PuHuq=<+Cm7F4(y;YQvt0Kr$?b78@JOL5HS2ndIQ3gMgD8*ou5}KY~c_t#Bkd&fe zRf38ae5NdBWC-OjrnpG(I3ie8L}KVDB_fajY9ca+h@TP_NeDB=PdyUmrD&;~p`fgs z7?6&dibTR^$QBet!gIk)^2>(9fc=77IT2A&LBvl%MP4Z=DCKS1E)jk5mx8{ZNlz(H z`DF;7-UY?vD5Ol^@)*b#M8G-CNgGY%l#wDV}BDHQRds@PKQ+{8yB%OAsVs#dA*UHTZ&??X0Em(S6CBMdP)b0}s$nJaWX20JAZtGK z3_O=jb>?=f10&FyA_YxmBpXRKKNS}jREiobj1)HTqY5dE9JU9O31xZYS1NRrk{?Sk zfn>(E>O3)^76ln#%zGA;ZCl9QPfNeq%<>nA&AG<>VDn>43i^tRD~ljH`0*3-Ry5dw zY0At~iisQKZc%es)Vj=?GON#-Ib-B=&rW@L(p`h@;85^Y^grUVXZ^yZg>-AHILkvrjxQW9qmm6NkggK5h2>GUxKP^(%I4TD^M7TPz&r zjg^iG5wnmvkzf$Ta>!2$B12R*5w9d>5an4ly`Ydt#v@Xp=BIQN67rKVknva~7U_AF zvfd^gh1gCwXB!B3^DMG?>(Bx_oP9`!)Q=?=?-Db~vI{BbJ{#bS3iANtN1`>|D$Isq zVcR5O9WiT}MtEaMGJ%N8OhRHRQnmHkO~U9ywd`J&fB^ToZk=%zLjou40Z%+*UJysn zQ(PVq)Pyw6Q(Ok-lVzN@g=>s~#Dw5Mu(Je_X$~gY$*f#CnXj4IfE}inCzco_Iwkf7 z2AI2Agn336PAD_46TgYoSP={ZhJX3HR5SC