Init.
This commit is contained in:
commit
4f9af8446c
7 changed files with 686 additions and 0 deletions
143
.gitignore
vendored
Normal file
143
.gitignore
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
in/
|
||||
out/
|
||||
cache/
|
||||
imageConfig.json
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
20
LICENSE
Normal file
20
LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
|||
Copyright 2021 Kevin Baensch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
134
colonycounter.py
Normal file
134
colonycounter.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
from typing import List, Generator, Optional, Tuple
|
||||
from fileutils import FILE, dslice, search, DIRIN, DIROUT, DIRCACHE
|
||||
import window
|
||||
from math import pi
|
||||
import cv2
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy import ndimage as ndi
|
||||
from skimage.filters import threshold_otsu, threshold_li
|
||||
from skimage import feature, measure, restoration, segmentation
|
||||
from pythreshold.global_th.entropy.kapur import kapur_threshold
|
||||
|
||||
PIXELS_TO_UM = 3.2 # 1 pixel equals 3.2 um
|
||||
|
||||
def rball(img, *argv, **args):
|
||||
return img - restoration.rolling_ball(img, *argv, **args)
|
||||
|
||||
|
||||
def watershed_new(img, blur: int, min_thresh: int, dist: int, min_size: int, min_roundness: float, min_mean_brightness: int, ignore: List[Tuple[int, int]]):
|
||||
MAX_VAL = 255 if img.dtype == "uint8" else 65535
|
||||
BRIGHT_MUL = 65535/255 if img.dtype == "uint8" else 1
|
||||
|
||||
c_img = cv2.cvtColor(img,cv2.COLOR_GRAY2RGB)
|
||||
|
||||
if blur > 0:
|
||||
b_img = cv2.GaussianBlur(img, (blur, blur), 0)
|
||||
thresh_val = kapur_threshold(b_img)
|
||||
thresh_val = thresh_val if thresh_val >= min_thresh else min_thresh
|
||||
thresh = cv2.threshold(cv2.bitwise_not(b_img), MAX_VAL-thresh_val, MAX_VAL, cv2.THRESH_BINARY_INV)[1]
|
||||
else:
|
||||
thresh_val = kapur_threshold(img)
|
||||
thresh_val = thresh_val if thresh_val >= min_thresh else min_thresh
|
||||
thresh = cv2.threshold(cv2.bitwise_not(img), MAX_VAL-thresh_val, MAX_VAL, cv2.THRESH_BINARY_INV)[1]
|
||||
thresh = segmentation.clear_border(thresh)
|
||||
thresh = ndi.binary_fill_holes(thresh)
|
||||
|
||||
distance = ndi.distance_transform_edt(thresh)
|
||||
if dist < 1:
|
||||
localMax = distance > distance.min()
|
||||
else:
|
||||
localMax = np.zeros_like(img, dtype=np.bool)
|
||||
localMax[tuple(feature.peak_local_max(distance, exclude_border=False, min_distance=dist, labels=thresh).T)] = True
|
||||
|
||||
|
||||
markers = ndi.label(localMax, structure=np.ones((3, 3)))[0]
|
||||
labels = segmentation.watershed(-img, markers, mask=thresh)
|
||||
|
||||
grain_num = 1
|
||||
meta = {
|
||||
"outlines": np.full_like(img, MAX_VAL, dtype=img.dtype),
|
||||
"data": [[ "Grain","Area","Mean","Min","Max","Circ", "Eccentricity", "IntDen","RawIntDen","AR","Round","Solidity" ]]
|
||||
}
|
||||
|
||||
for prop, label in list(zip(measure.regionprops(labels, intensity_image=img), np.unique(labels)[1:]))[:50]:
|
||||
bbox = prop["bbox"]
|
||||
|
||||
if prop["area"]*PIXELS_TO_UM**2 < min_size \
|
||||
or 1-prop["eccentricity"] < min_roundness \
|
||||
or prop["mean_intensity"]*BRIGHT_MUL < min_mean_brightness*257 \
|
||||
or grain_num > 50 \
|
||||
or any([bbox[1] <= x <= bbox[3] and bbox[0] <= y <= bbox[2]
|
||||
for (x, y) in ignore]):
|
||||
continue
|
||||
|
||||
# Create mask of image size and mark labeled area
|
||||
mask = np.zeros(img.shape, dtype=img.dtype)
|
||||
mask[labels == label] = MAX_VAL
|
||||
# cnts = measure.find_contours(mask.copy())
|
||||
# print(cnts)
|
||||
# c_img[cnts] = (65535,65535)
|
||||
# measure.drawContours()
|
||||
|
||||
# c_img = segmentation.mark_boundaries(c_img, mask, (0,255,255))
|
||||
# Create and draw contour around labeled area
|
||||
if cv2.__version__.startswith("3."):
|
||||
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
|
||||
cv2.CHAIN_APPROX_SIMPLE)[1]
|
||||
else:
|
||||
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
|
||||
cv2.CHAIN_APPROX_SIMPLE)[0]
|
||||
|
||||
c_img = cv2.drawContours(c_img, cnts, -1, (0,MAX_VAL,MAX_VAL), 1)
|
||||
meta["outlines"] = cv2.drawContours(meta["outlines"], cnts, -1, (0,0), 1)
|
||||
c = max(cnts, key=cv2.contourArea)
|
||||
(x,y), r = cv2.minEnclosingCircle(c)
|
||||
x = int(float(x) + float(r)/2)
|
||||
y = int(float(y) + float(r)/2)
|
||||
cv2.putText(c_img, str(grain_num), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
|
||||
cv2.putText(meta["outlines"], str(grain_num), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0), 1)
|
||||
meta["data"].append([
|
||||
grain_num,
|
||||
round(prop["area"]*PIXELS_TO_UM**2, 4),
|
||||
round(prop["mean_intensity"]*BRIGHT_MUL, 4),
|
||||
round(prop["min_intensity"]*BRIGHT_MUL, 4),
|
||||
round(prop["max_intensity"]*BRIGHT_MUL, 4),
|
||||
(1 if (4 * pi * prop.area) / (prop.perimeter_crofton * prop.perimeter_crofton) >=1 else round((4 * pi * prop.area) / (prop.perimeter_crofton * prop.perimeter_crofton), 4)),
|
||||
round(1-prop["eccentricity"], 4), # 4*pi*(prop["area"])/prop["perimeter"]**2,
|
||||
round((prop["area"]*PIXELS_TO_UM**2)*prop["mean_intensity"]*BRIGHT_MUL, 4),
|
||||
round(prop["intensity_image"].sum()*BRIGHT_MUL, 4),
|
||||
(None if prop["minor_axis_length"] == 0 else round(prop["major_axis_length"]/prop["minor_axis_length"], 4)),
|
||||
(None if prop["major_axis_length"] == 0 else round(4*prop["area"]*PIXELS_TO_UM**2/(pi*prop["major_axis_length"]**2)/10, 4)),
|
||||
round(prop["solidity"], 4)
|
||||
])
|
||||
grain_num += 1
|
||||
return c_img, meta
|
||||
|
||||
def main():
|
||||
"""
|
||||
tif metadata documentation:
|
||||
threshold-low -> lower bound bit range (0)
|
||||
threshold-high -> upper bound bit range (65535)
|
||||
https://docs.opencv.org/3.0-beta/modules/core/doc/operations_on_arrays.html#cv2.normalize
|
||||
"""
|
||||
files = [FILE(p) for p in search([DIRIN]) if p.endswith(".tif")]
|
||||
if files != []:
|
||||
files.sort()
|
||||
normalize = ["normalized", (cv2.normalize, [], {"dst": None, "alpha": 0, "beta": 65535, "norm_type": cv2.NORM_MINMAX}, True)]
|
||||
to8bit = ["8bit", (cv2.convertScaleAbs, [] , {"alpha": 255.0/65535.0}, True)]
|
||||
rollingball = ["rball", (rball, [], {"radius": 50.0}, True)]
|
||||
|
||||
wat = ["wshed", (watershed_new, [], {"blur": 0, "dist": 0, "min_thresh": 20, "min_size": 250, "min_roundness": 0.2, "min_mean_brightness": 50, "ignore": []}, False)]
|
||||
[f.opqueue.__init__([normalize, to8bit, rollingball, wat]) for f in files]
|
||||
[f.apply(dslice(f.opqueue,None,-1)) for f in files]
|
||||
|
||||
# [(f.apply(f.opqueue), f.save(True, f.opqueue)) for f in files]
|
||||
window.init(files)
|
||||
window.reset()
|
||||
return True
|
||||
print(f"ERR: No files in {DIRIN}, please add some.")
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
if main():
|
||||
window.run()
|
122
fileutils.py
Normal file
122
fileutils.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
import re
|
||||
from collections import OrderedDict
|
||||
from os import path, makedirs, listdir
|
||||
from typing import Callable, Dict, Optional, Tuple, List, Generator, Any, Union
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from cv2 import imread, imwrite
|
||||
|
||||
DIRIN = path.abspath("in")
|
||||
DIROUT = path.abspath("out")
|
||||
DIRCACHE = path.abspath("cache")
|
||||
|
||||
[makedirs(d, exist_ok=True) for d in [DIRIN, DIROUT, DIRCACHE]]
|
||||
|
||||
def dslice(odict: Dict[Any, Any], start: Optional[int] = None, end: Optional[int] = None) -> Dict[Any, Any]:
|
||||
return OrderedDict([
|
||||
(k, odict[k]) for k in list(odict.keys())[start:end]
|
||||
])
|
||||
|
||||
def lval(odict: Dict[Any, Any]) -> Any:
|
||||
return odict[list(odict.keys())[-1]]
|
||||
|
||||
def summarize(dlist):
|
||||
summary = pd.concat(dlist)
|
||||
summary.sort_values(["Hour", "Culture", "Label", "Grain"], ignore_index=True, inplace=True)
|
||||
return summary
|
||||
|
||||
|
||||
class FILE:
|
||||
def __init__(self, fpath: str, esum: Optional[str] = None):
|
||||
self.path, self.fname = path.split(path.relpath(fpath, start=DIRIN))
|
||||
self.fname, self.fext = path.splitext(self.fname)
|
||||
self.opqueue: Dict[str, Tuple[str, Callable, List[int]]] = OrderedDict()
|
||||
self.meta: Optional[pd.DataFrame] = None
|
||||
self.outlines: Optional[np.ndarray] = None
|
||||
self.load({})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return path.join(self.path, "".join([self.fname, self.fext]))
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
return [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', repr(self))] > \
|
||||
[int(c) if c.isdigit() else c for c in re.split('([0-9]+)', repr(other))]
|
||||
|
||||
def apply(self, opts: Dict[str, Tuple[str, Callable, List[int]]]):
|
||||
if self.load(opts):
|
||||
pass
|
||||
else:
|
||||
self.apply(dslice(opts, None, -1))
|
||||
try:
|
||||
self.data = lval(opts)[0](self.data, *lval(opts)[1], **lval(opts)[2])
|
||||
if type(self.data) is tuple:
|
||||
self.meta = pd.DataFrame(None if self.data[1]["data"][1:] == [] else self.data[1]["data"][1:], columns=self.data[1]["data"][0])
|
||||
self.meta.insert(loc=0, column="Label", value="".join([self.fname, self.fext]))
|
||||
spath = self.path.split(path.sep)
|
||||
if len(spath) > 2 and spath[-1] in ["BFP", "YFP"] and spath[-2][:-1].isdigit():
|
||||
hour, culture = self.path.split(path.sep)[-2:]
|
||||
hour = int(hour[:-1])
|
||||
else:
|
||||
hour, culture = None, None
|
||||
self.meta.insert(loc=0, column="Hour", value=hour)
|
||||
self.meta.insert(loc=1, column="Culture", value=culture)
|
||||
|
||||
self.outlines = self.data[1]["outlines"]
|
||||
self.data = self.data[0]
|
||||
if lval(opts)[3]:
|
||||
self.save(False, opts)
|
||||
except SyntaxError as error:
|
||||
print(f"Error: {error}")
|
||||
|
||||
def getOpStr(self, opts: Dict[str, Tuple[str, Callable, List[int]]]) -> str:
|
||||
return ';'.join([f"{x}_{','.join([str(y) for y in opts[x][1]])}" for x in opts.keys()])
|
||||
|
||||
def getName(self, cache: bool, opts: Dict[str, Tuple[str, Callable, List[int]]]) -> str:
|
||||
return f"{self.fname}{(cache and self.getOpStr(opts) + '.png') or self.fext}"
|
||||
|
||||
def save(self, toDirOut: bool, opts: Dict[str, Tuple[str, Callable, List[int]]]):
|
||||
if toDirOut:
|
||||
if not path.exists(path.join(DIROUT, self.path)):
|
||||
makedirs(path.join(DIROUT, self.path))
|
||||
imwrite(path.join(DIROUT, self.path, f"{self.fname}.jpg"), self.data)
|
||||
if self.outlines is not None:
|
||||
imwrite(path.join(DIROUT, self.path, f"{self.fname}.outlines.jpg"), self.outlines)
|
||||
if self.meta is not None:
|
||||
print(path.join(DIROUT, self.path, f"{self.fname}.csv"))
|
||||
self.meta.to_csv(path.join(DIROUT, self.path, f"{self.fname}.csv"), index=False)
|
||||
# [l.insert(1, self.getName(False, {})) for l in self.meta["data"][1:]]
|
||||
# with open(path.join(DIROUT, self.path, f"{self.fname}.csv"), "w") as f:
|
||||
# [f.write(",".join([str(e) for e in l]) + "\n") for l in self.meta["data"]]
|
||||
else:
|
||||
if not path.exists(path.join(DIRCACHE, self.path)):
|
||||
makedirs(path.join(DIRCACHE, self.path))
|
||||
imwrite(path.join(DIRCACHE, self.path, self.getName(True, opts)), self.data)
|
||||
|
||||
def load(self, opts: Dict[str, Tuple[str, Callable, List[int]]]) -> bool:
|
||||
if opts == {} and path.exists(path.join(DIRIN, self.path, self.getName(False, []))):
|
||||
self.data = imread(path.join(DIRIN, self.path, self.getName(False, [])), -1)
|
||||
return True
|
||||
if path.exists(path.join(DIRCACHE, self.path, self.getName(True, opts))):
|
||||
self.data = imread(path.join(DIRCACHE, self.path, self.getName(True, opts)), -1)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def search(pathlist: List[str]) -> Generator[str, None, None]:
|
||||
"""
|
||||
Generate file objects from given list of Paths.
|
||||
|
||||
+------------+
|
||||
| Parameters |
|
||||
+------------+
|
||||
| pathlist: List[str]
|
||||
| List of files and directories.
|
||||
"""
|
||||
for fpath in pathlist:
|
||||
if path.isfile(fpath):
|
||||
yield path.abspath(fpath)
|
||||
continue
|
||||
if path.isdir(fpath):
|
||||
yield from search([path.join(fpath, x) for x in listdir(fpath)])
|
||||
continue
|
||||
print(f"[WARN]: No such file or directory: {fpath}")
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
numpy
|
||||
opencv-python == 4.3.0
|
||||
pythreshold
|
||||
scikit-image
|
||||
scipy
|
37
shell.nix
Normal file
37
shell.nix
Normal file
|
@ -0,0 +1,37 @@
|
|||
{ pkgs ? import /nixpkgs {} }:
|
||||
|
||||
let
|
||||
pythreshold = pkgs.python3Packages.buildPythonPackage rec {
|
||||
pname = "pythreshold";
|
||||
version = "0.3.1";
|
||||
|
||||
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||
numpy
|
||||
scipy
|
||||
scikitimage
|
||||
matplotlib
|
||||
pkgs.python3Packages.opencv4
|
||||
];
|
||||
src = pkgs.python3Packages.fetchPypi {
|
||||
inherit pname version;
|
||||
sha256 = "149f4dkx6dm8rlh3disas9xpr13cfglpkrqfx2manaahgwkgpx12";
|
||||
};
|
||||
preBuild = ''
|
||||
substituteInPlace setup.py --replace "'opencv-python'" ""
|
||||
'';
|
||||
};
|
||||
in pkgs.mkShell rec {
|
||||
name = "ColonyCounter";
|
||||
version = "0.1";
|
||||
|
||||
pyEnv = pkgs.python38.withPackages (ps: with pkgs.python3Packages; [
|
||||
numpy
|
||||
pandas
|
||||
pythreshold
|
||||
scikitimage
|
||||
scipy
|
||||
opencv4
|
||||
]);
|
||||
|
||||
nativeBuildInputs = [ pyEnv ];
|
||||
}
|
225
window.py
Normal file
225
window.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
from copy import deepcopy
|
||||
import tkinter as tk
|
||||
from os.path import join
|
||||
from PIL import Image, ImageTk
|
||||
from cv2 import convertScaleAbs, cvtColor, COLOR_BGR2RGB
|
||||
from json import load, dump
|
||||
from fileutils import dslice, summarize, DIROUT
|
||||
|
||||
IMAGES = []
|
||||
CURIMINDEX = 0
|
||||
CURQUEUE = {}
|
||||
WIDTH = 1028
|
||||
HEIGHT = 1080
|
||||
|
||||
def init(imgList):
|
||||
global CURQUEUE
|
||||
IMAGES.extend(imgList)
|
||||
CURQUEUE = deepcopy(IMAGES[CURIMINDEX].opqueue)
|
||||
loadCMPImage()
|
||||
loadImage()
|
||||
|
||||
def convertImage(img):
|
||||
# Currently only expect grayscale and RGB images
|
||||
if img.dtype == "uint16":
|
||||
img = convertScaleAbs(img, alpha=255.0/65535.0)
|
||||
if len(img.shape) > 2:
|
||||
img = cvtColor(img, COLOR_BGR2RGB)
|
||||
img = Image.fromarray(img)
|
||||
return ImageTk.PhotoImage(img)
|
||||
|
||||
|
||||
def loadImage(event=None):
|
||||
IMAGES[CURIMINDEX].apply(CURQUEUE)
|
||||
label = IMAGES[CURIMINDEX].__repr__()
|
||||
img = convertImage(IMAGES[CURIMINDEX].data)
|
||||
IMLABEL.configure(text=label)
|
||||
IMLABEL.text = label
|
||||
CURIMAGE.configure(image=img)
|
||||
CURIMAGE.image = img
|
||||
|
||||
def loadCMPImage(event=None):
|
||||
IMAGES[CURIMINDEX].apply(dslice(CURQUEUE, 0, int(S_CMPIMAGE.get())))
|
||||
img = convertImage(IMAGES[CURIMINDEX].data)
|
||||
CMPIMAGE.configure(image=img)
|
||||
CMPIMAGE.image = img
|
||||
|
||||
def loadImageNext(event=None):
|
||||
global CURIMINDEX
|
||||
CURIMINDEX = (CURIMINDEX+1) % (len(IMAGES))
|
||||
reset()
|
||||
loadCMPImage()
|
||||
|
||||
def loadImagePrevious(event=None):
|
||||
global CURIMINDEX
|
||||
CURIMINDEX = (CURIMINDEX-1) % (len(IMAGES))
|
||||
reset()
|
||||
loadCMPImage()
|
||||
|
||||
def reset(event=None):
|
||||
global CURQUEUE
|
||||
CURQUEUE = deepcopy(IMAGES[CURIMINDEX].opqueue)
|
||||
if "wshed" in CURQUEUE:
|
||||
S_BLURSIZE.set(CURQUEUE["wshed"][2]["blur"])
|
||||
S_THRESH.set(CURQUEUE["wshed"][2]["min_thresh"])
|
||||
S_SIZE.set(CURQUEUE["wshed"][2]["min_size"])
|
||||
S_DISTANCE.set(CURQUEUE["wshed"][2]["dist"])
|
||||
S_MBRIGHT.set(CURQUEUE["wshed"][2]["min_mean_brightness"])
|
||||
S_ROUND.set(CURQUEUE["wshed"][2]["min_roundness"])
|
||||
loadImage()
|
||||
|
||||
def apply(event=None):
|
||||
IMAGES[CURIMINDEX].opqueue = deepcopy(CURQUEUE)
|
||||
|
||||
def set_blursize(ksize):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["blur"] = int(ksize)-1
|
||||
loadImage()
|
||||
|
||||
def set_min_thresh(min_thresh):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["min_thresh"] = int(min_thresh)
|
||||
loadImage()
|
||||
|
||||
def set_min_size(min_size):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["min_size"] = int(min_size)
|
||||
loadImage()
|
||||
|
||||
def set_distance(dist):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["dist"] = int(dist)
|
||||
loadImage()
|
||||
|
||||
def set_min_mean_brightness(min_mean_brightness):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["min_mean_brightness"] = int(min_mean_brightness)
|
||||
loadImage()
|
||||
|
||||
def set_min_roundness(min_roundness):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["min_roundness"] = float(min_roundness)
|
||||
loadImage()
|
||||
|
||||
def set_comp(qslice):
|
||||
loadCMPImage()
|
||||
|
||||
def add_pos(event):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["ignore"].append((event.x, event.y))
|
||||
loadImage()
|
||||
|
||||
def remove_pos(event):
|
||||
if "wshed" in CURQUEUE:
|
||||
# print(f"box_x: {event.x-5}, {event.x+5}")
|
||||
# print(f"box_y: {event.y-5}, {event.y+5}")
|
||||
CURQUEUE["wshed"][2]["ignore"] = [
|
||||
pos for pos in
|
||||
CURQUEUE["wshed"][2]["ignore"]
|
||||
if (((event.x-15) < pos[0] < (event.x+15)) is False)
|
||||
and (((event.y-15) < pos[1] < (event.y+15)) is False)
|
||||
]
|
||||
loadImage()
|
||||
|
||||
def clear_pos(event):
|
||||
if "wshed" in CURQUEUE:
|
||||
CURQUEUE["wshed"][2]["ignore"] = []
|
||||
loadImage()
|
||||
|
||||
def export(event=None):
|
||||
[(f.apply(f.opqueue), f.save(True, f.opqueue)) for f in IMAGES]
|
||||
summary = summarize([f.meta for f in IMAGES])
|
||||
summary.to_csv(join(DIROUT, "summary.csv"), index=False)
|
||||
summary.groupby(["Hour", "Culture"]).mean().round(4).to_csv(join(DIROUT, "summary_mean.csv"))
|
||||
summary.groupby(["Hour", "Culture"]).std().round(4).to_csv(join(DIROUT, "summary_std.csv"))
|
||||
|
||||
def settings_load(event=None):
|
||||
with open("imageConfig.json", "r") as f:
|
||||
settings = load(f)
|
||||
for (n, i) in enumerate(IMAGES):
|
||||
if repr(i) in settings:
|
||||
[
|
||||
IMAGES[n].opqueue.__setitem__(k,
|
||||
[i.opqueue[k][0]] +
|
||||
settings[repr(i)][k]
|
||||
)
|
||||
for k in i.opqueue
|
||||
if k in settings[repr(i)]
|
||||
]
|
||||
reset()
|
||||
|
||||
def settings_save(event=None):
|
||||
with open("imageConfig.json", "w") as f:
|
||||
dump(dict([[repr(i), dict([[o, i.opqueue[o][1:]] for o in i.opqueue])] for i in IMAGES]), f)
|
||||
|
||||
def run():
|
||||
ROOT.mainloop()
|
||||
|
||||
|
||||
ROOT = tk.Tk()
|
||||
ROOT.wm_title("FP Analysis")
|
||||
ROOT.geometry(f"{WIDTH}x{HEIGHT}")
|
||||
ROOT.rowconfigure(0, weight=3)
|
||||
ROOT.rowconfigure(1, weight=10)
|
||||
ROOT.rowconfigure(2, weight=1)
|
||||
ROOT.bind("<Left>", loadImagePrevious)
|
||||
ROOT.bind("<Right>", loadImageNext)
|
||||
ROOT.bind("q", loadImagePrevious)
|
||||
ROOT.bind("w", loadImageNext)
|
||||
ROOT.bind("a", apply)
|
||||
ROOT.bind("r", reset)
|
||||
ROOT.bind("s", settings_save)
|
||||
ROOT.bind("l", settings_load)
|
||||
ROOT.bind("e", export)
|
||||
ROOT.bind("c", clear_pos)
|
||||
|
||||
F_SLIDERS = tk.Frame(ROOT, width=WIDTH, height=50)
|
||||
F_SLIDERS.grid(row=0)
|
||||
F_IMAGE = tk.Canvas(ROOT, width=WIDTH, height=500)
|
||||
F_IMAGE.grid(row=1)
|
||||
F_IMAGE.rowconfigure(0, weight=0)
|
||||
F_IMAGE.rowconfigure(1, weight=10)
|
||||
F_BUTTONS = tk.Frame(ROOT, width=WIDTH, height=50)
|
||||
F_BUTTONS.grid(row=2)
|
||||
|
||||
|
||||
S_BLURSIZE = tk.Scale(F_SLIDERS, label='Blur Size', from_=0, to=255, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=25, resolution=2, command=set_blursize)
|
||||
S_BLURSIZE.grid(row=0, column=0, sticky="N")
|
||||
S_THRESH = tk.Scale(F_SLIDERS, label='Min Threshold', from_=0, to=255, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=25, resolution=1, command=set_min_thresh)
|
||||
S_THRESH.grid(row=1, column=0, sticky="N")
|
||||
S_DISTANCE = tk.Scale(F_SLIDERS, label='Distance Transform', from_=0, to=50, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=10, resolution=1, command=set_distance)
|
||||
S_DISTANCE.grid(row=2, column=0, sticky="N")
|
||||
S_SIZE = tk.Scale(F_SLIDERS, label='Filter: Min Size', from_=0, to=500, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=25, resolution=1, command=set_min_size)
|
||||
S_SIZE.grid(row=3, column=0, sticky="N")
|
||||
S_MBRIGHT = tk.Scale(F_SLIDERS, label='Filter: Min Mean Brightness', from_=0, to=255, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=25, resolution=1, command=set_min_mean_brightness)
|
||||
S_MBRIGHT.grid(row=4, column=0, sticky="N")
|
||||
S_ROUND = tk.Scale(F_SLIDERS, label='Filter: Min Roundness', from_=0, to=1, orient=tk.HORIZONTAL, length=WIDTH-10, showvalue=True, tickinterval=0.1, resolution=0.05, command=set_min_roundness)
|
||||
S_ROUND.grid(row=5, column=0, sticky="N")
|
||||
S_CMPIMAGE = tk.Scale(F_IMAGE, label=None, from_=0, to=4, orient=tk.HORIZONTAL, length=WIDTH/2-10, showvalue=False, tickinterval=1, resolution=1, command=set_comp)
|
||||
S_CMPIMAGE.grid(row=0, column=0, sticky="N")
|
||||
|
||||
IMLABEL = tk.Label(F_IMAGE, text="", height=1)
|
||||
IMLABEL.grid(row=0, column=1)
|
||||
CURIMAGE = tk.Label(F_IMAGE, image=None)
|
||||
CURIMAGE.grid(row=1, column=1)
|
||||
CMPIMAGE = tk.Label(F_IMAGE, image=None)
|
||||
CMPIMAGE.grid(row=1, column=0)
|
||||
CURIMAGE.bind("<Button 1>", add_pos)
|
||||
CURIMAGE.bind("<Button 3>", remove_pos)
|
||||
CURIMAGE.bind("<Button-1>", add_pos)
|
||||
CURIMAGE.bind("<Button-3>", remove_pos)
|
||||
|
||||
B_RESET = tk.Button(F_BUTTONS, text="Reset", command=reset)
|
||||
B_RESET.grid(row=0, column=0, sticky="W")
|
||||
B_APPLY = tk.Button(F_BUTTONS, text="Apply", command=apply)
|
||||
B_APPLY.grid(row=0, column=1, sticky="E")
|
||||
B_PREVIOUS = tk.Button(F_BUTTONS, text="Previous", command=loadImagePrevious)
|
||||
B_PREVIOUS.grid(row=0, column=2, sticky="E")
|
||||
B_NEXT = tk.Button(F_BUTTONS, text="Next", command=loadImageNext)
|
||||
B_NEXT.grid(row=0, column=3, sticky="E")
|
||||
B_SLOAD = tk.Button(F_BUTTONS, text="Load Settings", command=settings_load)
|
||||
B_SLOAD.grid(row=0, column=4, sticky="E")
|
||||
B_SSAVE = tk.Button(F_BUTTONS, text="Save Settings", command=settings_save)
|
||||
B_SSAVE.grid(row=0, column=5, sticky="E")
|
||||
B_EXPORT = tk.Button(F_BUTTONS, text="Export Images", command=export)
|
||||
B_EXPORT.grid(row=0, column=6, sticky="E")
|
Loading…
Reference in a new issue