Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions kalmus/command_line_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@ def main(args=sys.argv[1:]):
rescale_frames_factor=args["rescale_frame_factor"])
barcode = barcode_generator.get_barcode()
barcode.save_as_json(filename=args["output_path"])


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions kalmus/command_line_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ def main(args=sys.argv[1:]):

# Use the default barcode and the barcode generator to Instantiate the Main window of the kalmus software (GUI)
MainWindow(barcode_tmp, barcode_gn, dpi=args["dpi"])


if __name__ == "__main__":
main()
23 changes: 18 additions & 5 deletions kalmus/tkinter_windows/SaveImageWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ def save_image(self):
showerror("File Name is Not Given", "Please specify the path to the saved image.")
return

# On Linux/X11 the file dialog does not auto-append the filtered extension, and a
# user typing the path by hand may also omit it. Default to .jpg so the call into
# matplotlib/PIL has something to dispatch on.
_, ext = os.path.splitext(filename)
if ext == "":
filename += ".jpg"

# Get which barcode to save
if self.barcode_option.get() == "Barcode 1":
barcode = self.barcode_1.get_barcode().astype("uint8")
Expand All @@ -137,11 +144,17 @@ def save_image(self):
barcode = cv2.resize(barcode, dsize=(int(self.resize_x_entry.get()), int(self.resize_y_entry.get())),
interpolation=cv2.INTER_NEAREST)

# Save the barcode with desirable color map based on its barcode type
if barcode_type == "Color":
plt.imsave(filename, barcode)
else:
plt.imsave(filename, barcode, cmap="gray")
# Save the barcode with desirable color map based on its barcode type. Surface any
# save error (unsupported extension, permission denied, ...) as a dialog instead of
# letting it tear up the Tk callback.
try:
if barcode_type == "Color":
plt.imsave(filename, barcode)
else:
plt.imsave(filename, barcode, cmap="gray")
except (ValueError, OSError) as exc:
showerror("Could Not Save Image", "Failed to save image to {:s}\n\n{:s}".format(filename, str(exc)))
return

# Quit the window
self.window.destroy()
Expand Down
5 changes: 3 additions & 2 deletions kalmus/tkinter_windows/gui_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,9 @@ def update_hist(barcode, ax, bin_step=5):
# Paint each bin with its corresponding color in hue
paint_hue_hist(bin_step, patches)
else:
# If the barcode type is brightness
N, bins, patches = ax.hist(barcode.brightness[:, 0], bins=(np.arange(0, 256, bin_step)))
# If the barcode type is brightness. `brightness` is stored 1-D (one scalar per
# sampled frame), so flatten defensively rather than indexing a phantom 2nd axis.
N, bins, patches = ax.hist(barcode.brightness.ravel(), bins=(np.arange(0, 256, bin_step)))

# Then paint each bin with its brightness intensity
paint_gray_hist(bin_step, patches, opacity=0.9)
Expand Down
21 changes: 18 additions & 3 deletions kalmus/utils/artist.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Utility artist """

import inspect
import numpy as np
import cv2
import csv
Expand All @@ -11,6 +12,17 @@
import scipy.stats as stats


# skimage>=0.26 deprecates `min_size` (removes objects with size < N) in favour of
# `max_size` (removes objects with size <= N). The equivalence is `max_size = N - 1`.
# This shim picks the right kwarg at import time so we work across both APIs.
if "max_size" in inspect.signature(remove_small_objects).parameters:
def _remove_objects_smaller_than(arr, threshold):
return remove_small_objects(arr, max_size=threshold - 1)
else:
def _remove_objects_smaller_than(arr, threshold):
return remove_small_objects(arr, min_size=threshold)


def compute_dominant_color(image, n_clusters=3, max_iter=10, threshold_error=1.0, attempts=10):
"""
Compute the dominant color of an input image using the kmeans clustering. The centers of the
Expand Down Expand Up @@ -109,8 +121,11 @@ def compute_mode_color(image, bin_size=10):
"""
image = flatten_image(image)
image = image // bin_size
mode_color, counts = stats.mode(image, axis=0)
return (mode_color[0] * bin_size), counts
# scipy>=1.11 changed stats.mode default to keepdims=False; pin the shape
# explicitly so callers that expect (channels,) keep working across versions.
mode_result = stats.mode(image, axis=0, keepdims=False)
mode_color, counts = mode_result.mode, mode_result.count
return mode_color * bin_size, counts


def compute_mean_color(image):
Expand Down Expand Up @@ -362,7 +377,7 @@ def watershed_segmentation(image, minimum_segment_size=0.0004, base_ratio=0.5, d
segment_min_size = int(num_of_pixels * minimum_segment_size)
# find continuous region which marked by gradient lower than critical gradient
markers = rank.gradient(denoised, disk(marker_disk_size)) < critical
markers = ndimage.label(remove_small_objects(markers, segment_min_size))[0]
markers = ndimage.label(_remove_objects_smaller_than(markers, segment_min_size))[0]

# process the watershed transformation
regions = watershed(gradient, markers)
Expand Down
44 changes: 33 additions & 11 deletions kalmus/utils/measure_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
""" Image Comparison Utility """

import Bio.pairwise2 as sequence_align
import numpy as np
from Bio.Align import PairwiseAligner
from skimage.color import rgb2hsv
from skimage.metrics import mean_squared_error, structural_similarity


def _build_aligner(mode, match_score, mismatch_penal, gap_penal, extending_gap_penal):
"""Construct a PairwiseAligner mirroring the pairwise2.align.{globalms,localms} scoring model.

pairwise2 applied the same gap penalty to both terminal and internal gaps; assigning
``open_gap_score`` / ``extend_gap_score`` on PairwiseAligner does the same.
"""
aligner = PairwiseAligner()
aligner.mode = mode
aligner.match_score = match_score
aligner.mismatch_score = mismatch_penal
aligner.open_gap_score = gap_penal
aligner.extend_gap_score = extending_gap_penal
return aligner


def nrmse_similarity(image_1, image_2, norm_mode="Min max"):
"""
Normalized root mean squared error (NRMSE).
Expand Down Expand Up @@ -61,10 +76,19 @@ def ssim_similarity(image_1, image_2, window_size=None):
image_1 = image_1.astype("float64")
image_2 = image_2.astype("float64")

# skimage requires explicit `data_range` for floating-point inputs since 0.21.
# Derive it from the joint value range so behaviour matches the prior int default.
data_range = max(image_1.max(), image_2.max()) - min(image_1.min(), image_2.min())
if data_range == 0:
data_range = 1.0

# scikit-image>=0.21 dropped the `multichannel` kwarg in favour of `channel_axis`.
if len(image_1.shape) == 2:
score = structural_similarity(image_1, image_2, win_size=window_size, multichannel=False)
score = structural_similarity(image_1, image_2, win_size=window_size,
channel_axis=None, data_range=data_range)
elif len(image_1.shape) > 2:
score = structural_similarity(image_1, image_2, win_size=window_size, multichannel=True)
score = structural_similarity(image_1, image_2, win_size=window_size,
channel_axis=-1, data_range=data_range)

# Renormalize [-1, 1] score to [0, 1] range
score += 1
Expand Down Expand Up @@ -264,12 +288,11 @@ def compare_needleman_wunsch(str_barcode_1, str_barcode_2, local_sequence_size=2
"""
assert len(str_barcode_1) == len(str_barcode_2), "The lengths of two barcodes have to be identical"

aligner = _build_aligner("global", match_score, mismatch_penal, gap_penal, extending_gap_penal)
scores = 0
for start_point in range(0, len(str_barcode_1), local_sequence_size):
scores += sequence_align.align.globalms(str_barcode_1[start_point:start_point + local_sequence_size],
str_barcode_2[start_point:start_point + local_sequence_size],
match_score, mismatch_penal, gap_penal, extending_gap_penal,
score_only=True)
scores += aligner.score(str_barcode_1[start_point:start_point + local_sequence_size],
str_barcode_2[start_point:start_point + local_sequence_size])

if normalized:
denom = len(str_barcode_1) * match_score
Expand Down Expand Up @@ -307,12 +330,11 @@ def compare_smith_waterman(str_barcode_1, str_barcode_2, local_sequence_size=200
"""
assert len(str_barcode_1) == len(str_barcode_2), "The lengths of two barcodes have to be identical"

aligner = _build_aligner("local", match_score, mismatch_penal, gap_penal, extending_gap_penal)
scores = 0
for start_point in range(0, len(str_barcode_1), local_sequence_size):
scores += sequence_align.align.localms(str_barcode_1[start_point:start_point + local_sequence_size],
str_barcode_2[start_point:start_point + local_sequence_size],
match_score, mismatch_penal, gap_penal, extending_gap_penal,
score_only=True)
scores += aligner.score(str_barcode_1[start_point:start_point + local_sequence_size],
str_barcode_2[start_point:start_point + local_sequence_size])

if normalized:
denom = len(str_barcode_1) * match_score
Expand Down
72 changes: 11 additions & 61 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,61 +1,11 @@
attrs==19.1.0
backcall==0.1.0
biopython==1.78
bleach>=3.3.0
certifi==2020.12.5
cycler==0.10.0
decorator==4.4.0
defusedxml==0.6.0
entrypoints==0.3
imageio==2.9.0
ipykernel==5.1.1
ipython>=7.16.3
ipython-genutils==0.2.0
ipywidgets==7.5.0
jedi==0.14.1
Jinja2>=2.11.3
joblib==1.0.0
jsonschema==3.0.1
jupyter==1.0.0
jupyter-console==6.0.0
kiwisolver==1.3.1
MarkupSafe==1.1.1
matplotlib==3.2.2
mistune==0.8.4
mpmath==1.1.0
nbconvert==5.5.0
nbformat==4.4.0
networkx==2.5
nose==1.3.7
notebook>=6.1.5
numpy>=1.21
opencv-python==4.4.0.46
pandocfilters==1.4.2
parso==0.5.1
pickleshare==0.7.5
Pillow>=8.1.1
prometheus-client==0.7.1
prompt-toolkit==2.0.9
Pygments>=2.7.4
pyparsing==2.4.1.1
pyrsistent==0.15.4
python-dateutil==2.8.0
pytz==2019.1
PyWavelets==1.1.1
pywinpty==0.5.5
qtconsole==4.5.2
scikit-image>=0.16.2
scikit-learn==0.24.0
scipy==1.5.4
Send2Trash==1.5.0
six==1.12.0
sympy==1.4
terminado==0.8.2
testpath==0.4.2
threadpoolctl==2.1.0
tifffile==2020.12.8
traitlets==4.3.2
wcwidth==0.1.7
webencodings==0.5.1
widgetsnbextension==3.5.0
wincertstore==0.2
# Runtime dependencies for kalmus. Mirrors setup.py install_requires.
# For the optional notebook/dev environment, install jupyter/ipykernel/nbconvert separately.
biopython>=1.80
kiwisolver>=1.4
matplotlib>=3.5
numpy>=1.23
opencv-python-headless>=4.6
pandas>=1.5
scikit-image>=0.21
scikit-learn>=1.1
scipy>=1.11
25 changes: 20 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
Expand All @@ -32,9 +35,21 @@
"Topic :: Software Development :: User Interfaces"
],
packages=find_packages(),
python_requires='>=3.7',
install_requires=['numpy', 'opencv-python', 'scikit-image>=0.16.2', 'matplotlib>=3.2.2',
'scikit-learn', 'biopython', 'scipy', 'kiwisolver>=1.3.1', 'pandas'],
python_requires='>=3.10',
install_requires=[
'numpy>=1.23',
# headless variant: same cv2 API, no X/GL system libs required.
# kalmus uses cv2 only for image-processing (VideoCapture, cvtColor,
# kmeans, grabCut, ...); the GUI is tkinter, never cv2.imshow.
'opencv-python-headless>=4.6',
'scikit-image>=0.21', # channel_axis kwarg replaces multichannel
'matplotlib>=3.5',
'scikit-learn>=1.1',
'biopython>=1.80', # PairwiseAligner is the supported aligner
'scipy>=1.11', # stats.mode keepdims default change handled
'kiwisolver>=1.4',
'pandas>=1.5',
],
entry_points={
'console_scripts': ['kalmus-gui=kalmus.command_line_gui:main',
'kalmus-generator=kalmus.command_line_generator:main'],
Expand Down
2 changes: 1 addition & 1 deletion tests/test_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_compute_mode_color(get_test_color_image):
image = get_test_color_image
color, count = artist.compute_mode_color(get_test_color_image)
assert color.shape == (3,)
assert count.shape == (1, 3)
assert count.shape == (3,)
assert np.all(count < image.size)


Expand Down