Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Complexity measure #134

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
6 changes: 5 additions & 1 deletion pylithics/scripts/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
find_lithic_contours, detect_lithic, process_image, data_output, \
get_scars_angles, find_arrows
from pylithics.src.plotting import plot_results, plot_thresholding
from pylithics.src.utils import pixulator, get_angles
from pylithics.src.utils import pixulator, get_angles, complexity_estimator


def run_pipeline(id_list, metadata_df, input_dir, output_dir, config_file, get_arrows):
Expand Down Expand Up @@ -115,6 +115,10 @@ def run_characterisation(input_dir, output_dir, config_file, arrows, debug=False
# find contours
contours = find_lithic_contours(binary_array, config_file)

# measure complexity on scars
contours = complexity_estimator(contours)


# if this lithic has arrows do processing to detect and measure arrow angle
if arrows:

Expand Down
44 changes: 44 additions & 0 deletions pylithics/src/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ def plot_results(id, image_array, contours_df, output_dir):
output_lithic = os.path.join(output_dir, id + "_lithium_angles.png")
plot_angles(image_array, contours_df, output_lithic)

# plot scar strike angle
output_lithic = os.path.join(output_dir, id + "_complexity_polygon_count.png")
plot_complexity(image_array, contours_df, output_lithic)



def plot_thresholding(image_array, threshold, binary_array, output_file=''):
Expand Down Expand Up @@ -279,3 +283,43 @@ def plot_template_arrow(image_array, template_array, value):
ax[1].set_yticks([])
plt.figtext(0.4, 0.9, str(value))
plt.show()

def plot_complexity(image_array, contours_df, output_path):
"""
Plot the contours from the lithic surfaces and display complexity and polygon count measurements.

Parameters
----------
image_array: array
Original image array (0 to 255)
contours_df: dataframe
Dataframe with detected contours and extra information about them.
output_path: str
Path to output directory to save processed images

"""
fig_x_size = fig_size(image_array)
fig, ax = plt.subplots(figsize=(fig_x_size, 20))
ax.imshow(image_array, cmap=plt.cm.gray)

# selecting only scars with a complexity measure > 0
contours_complexity_df = contours_df[(contours_df['parent_index'] != -1) & (contours_df['complexity']>0)]
cmap_list = plt.cm.get_cmap('tab20', contours_complexity_df.shape[0])

if contours_complexity_df.shape[0] == 0:
warnings.warn("Warning: No scars with complexity measure, no complexity output figure will be saved.'")
return None

i = 0
for contour, complexity, polygon_count in \
contours_complexity_df[['contour', 'complexity','polygon_count']].itertuples(index=False):
text = "Complexity: " + str(complexity)+", Polygon Count: "+str(polygon_count)
ax.plot(contour[:, 0], contour[:, 1], label=text, linewidth=5, color=cmap_list(i))
i = i + 1

ax.set_xticks([])
ax.set_yticks([])
plt.legend(bbox_to_anchor=(1.02, 0), loc="lower left", borderaxespad=0, fontsize=11)
plt.title("Scar complexity and polygon count measurements", fontsize=30)
plt.savefig(output_path)
plt.close(fig)
6 changes: 3 additions & 3 deletions pylithics/src/read_and_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ def data_output(contour_df, config_file):

scars_objects_list = []
scar_id = 0
for index, area_px, area_mm, width_mm, height_mm, angle, polygon_count in scars_df[
for index, area_px, area_mm, width_mm, height_mm, angle, polygon_count, complexity in scars_df[
['index', 'area_px', 'area_mm',
'width_mm', 'height_mm', 'angle', 'polygon_count']].itertuples(index=False):
'width_mm', 'height_mm', 'angle', 'polygon_count','complexity']].itertuples(index=False):
scars_objects = {}

scars_objects['scar_id'] = scar_id
Expand All @@ -224,6 +224,7 @@ def data_output(contour_df, config_file):
scars_objects['total_area_px'] / outer_objects['total_area_px'], 2)
scars_objects['scar_angle'] = angle
scars_objects["polygon_count"] = polygon_count
scars_objects["complexity"] = complexity

scars_objects_list.append(scars_objects)
scar_id = scar_id + 1
Expand Down Expand Up @@ -312,7 +313,6 @@ def get_scars_angles(image_array, contour_df, templates = pd.DataFrame()):

if templates.shape[0] == 0:
# if there is no templates in the dataframe assing nan to angles.
contour_df['arrow_index'] = -1
contour_df['angle'] = np.nan

# TODO: DO SOMETHING WITH RIPPLES
Expand Down
89 changes: 89 additions & 0 deletions pylithics/src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import scipy.ndimage as ndi
import pylithics.src.plotting as plot
import math
from scipy.spatial.distance import cdist


def mask_image(binary_array, contour, innermask=False):
Expand Down Expand Up @@ -658,3 +659,91 @@ def shape_detection(contour):
shape = "arrow"
# otherwise, we assume the shape is an arrow
return shape, vertices

def complexity_estimator(contour_df):
"""

Function that estimate a complexity measure. Complexity is measured as the number of adjacent contours
for each contour.

Parameters
----------
contour_df: dataframe
Dataframe with all contour information for an image.
Returns
-------

A copy of the contour_df dataframe with a new measure of complexity

"""

adjacency_list = []
for i in range(0, contour_df.shape[0]):
if contour_df.iloc[i]["parent_index"] == -1:
adjacency_list.append(0)
else:
# list coordinates for the contour we are interested on
contour_coordinate = contour_df.iloc[i]["contour"]

# list of coordinates of each of the siblings that we are interested on (list of list)
contour_coordinate_siblings = contour_df[contour_df["parent_index"] == contour_df.iloc[i]["parent_index"]]['contour'].values

count = 0
for sibling_contour in contour_coordinate_siblings:

# compare contour_coordinate with sibling_contour
adjacent = complexity_measure(contour_coordinate,sibling_contour)

if adjacent == True:
count = count + 1

adjacency_list.append(count)

contour_df['complexity'] = adjacency_list

return contour_df


def complexity_measure(contour_coordinates1, contour_coordinates2):
"""
Decide if two contours are adjacent based on distance between its coordinates.

Parameters
----------
contour_coordinates1: list of lists
Pixel coordinates for a contour of a single flake scar
or outline of a lithic object detected by contour finding
contour_coordinates2: list of lists
Pixel coordinates for a contour of a single flake scar
or outline of a lithic object detected by contour finding

Returns
-------

A boolean

"""

# if they are the same contour they are not adjacent
if np.array_equal(contour_coordinates1, contour_coordinates2):
return False
else:
# get minimum distance between contours
min_dist = np.min(cdist(contour_coordinates1, contour_coordinates2))

# if the minimum distance found is less than a threshold then they are adjacent
if min_dist < 70:
return True
else:
return False











10 changes: 8 additions & 2 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import yaml
from pylithics.src.read_and_process import read_image, detect_lithic, \
find_lithic_contours, process_image, get_scars_angles, data_output, find_arrows
from pylithics.src.utils import get_angles
from pylithics.src.utils import get_angles, complexity_estimator


def test_pipeline():
Expand All @@ -28,6 +28,9 @@ def test_pipeline():
# find contours
contours = find_lithic_contours(binary_array, config_file)

# add complexity measure
contours = complexity_estimator(contours)

# in case we dont have arrows
contours = get_scars_angles(image_processed, contours)

Expand Down Expand Up @@ -60,6 +63,9 @@ def test_arrow_pipeline():
# find contours
contours = find_lithic_contours(binary_array, config_file)

# add complexity measure
contours = complexity_estimator(contours)

# get the templates for the arrows
templates = find_arrows(image_array, image_processed, False)

Expand All @@ -70,5 +76,5 @@ def test_arrow_pipeline():
contours_final = get_scars_angles(image_processed, contours, arrow_df)

assert len(templates) == 4
assert contours_final.shape == (11, 15)
assert contours_final.shape == (11, 16)
assert arrow_df.shape == (4, 2)
18 changes: 17 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pylithics.src.utils import mask_image, contour_characterisation, classify_distributions, shape_detection, \
get_high_level_parent_and_hierarchy, pixulator, classify_surfaces, subtract_masked_image, measure_vertices, \
get_angles, \
measure_arrow_angle, contour_selection
measure_arrow_angle, contour_selection, complexity_estimator

# Global loads for all tests
image_array = read_image(os.path.join('tests', 'test_images'), 'test')
Expand Down Expand Up @@ -196,3 +196,19 @@ def test_shape_detection():
shape = shape_detection(cont)

assert shape == ('arrow', 4)

def test_complexity_estimator():

image_processed = process_image(image_array, config_file)

config_file['conversion_px'] = 0.1 # hardcoded for now
binary_edge_sobel, _ = detect_lithic(image_processed, config_file)

contours = find_lithic_contours(binary_edge_sobel, config_file)

contour_complexity = complexity_estimator(contours)

assert contour_complexity['complexity'].iloc[7] == 4