Commit 8098e981 authored by Tamino Huxohl's avatar Tamino Huxohl
Browse files

initial commit

parents
*__pycache__/
*.ipynb_checkpoints/
venv/
build/
dist/
*.egg-info/
# Stain Detection
Software für die Erkennung von Flecken mittels neuronalen Netzen der Salienzerkennung.
## Installation
Die Software ist in der Programmiersprache [Python](https://www.python.org/) geschrieben.
Dementsprechend wird ein entsprechender Interpreter benötigt.
Außerdem nutzt die Software andere Python-Bibliotheken, wie [PyTorch](https://pytorch.org/), [NumPy](https://numpy.org/) und [OpenCV](https://opencv.org/).
Um diese zu installieren wird der Paketmanager [PIP](https://pip.pypa.io/) genutzt.
Des Weiteren ist es in Python üblich die Abhängigkeiten einzelner Projekte in virtuellen Umgebungen zu trennen.
Dafür kommt hier [venv](https://docs.python.org/3/library/venv.html) zum Einsatz.
1. Installiere Interpreter, Paketmanager und Manager für virtuelle Umgebungen: `sudo apt install python3 python3-pip python3-venv`
2. Erstelle eine virtuelle Umgebung: `python -m venv venv`
3. Laden der virtuellen Umgebung: `source venv/bin/activate`
4. Installation der Software: `./install.sh`
Mit dem Script `install.sh` wird die Software für die Fleckerkennung und deren Abhängigkeiten in der virtuellen Umgebung installiert.
Das bedeutet auch, dass man vor dem verwenden der Software immer sicherstellen muss, dass die virtuelle Umgebung geladen ist.
#!/bin/sh
pip install build && \
python -m build . && \
pip install .
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
cycler==0.11.0
fonttools==4.28.5
imageio==2.13.5
kiwisolver==1.3.2
matplotlib==3.5.1
networkx==2.6.3
numpy==1.22.1
opencv-python==4.5.5.62
packaging==21.3
pandas==1.3.5
Pillow==9.0.0
pyparsing==3.0.6
python-dateutil==2.8.2
pytz==2021.3
PyWavelets==1.2.0
scikit-image==0.19.1
scipy==1.7.3
six==1.16.0
tifffile==2021.11.2
torch==1.10.1
torchvision==0.11.2
tqdm==4.62.3
typing-extensions==4.0.1
[metadata]
name = model_type
version = 0.0.1
[options]
packages = find:
install_requires =
matplotlib==3.5.1
numpy==1.22.1
opencv-python==4.5.5.62
pandas==1.3.5
Pillow==9.0.0
scikit-image==0.19.1
scipy==1.7.3
torch==1.10.1
torchvision==0.11.2
tqdm==4.62.3
from setuptools import setup
setup()
#!/bin/bash
pip install -r requirements.txt -f https://download.pytorch.org/whl/torch_stable.html
import argparse
import enum
from functools import reduce
import os
import cv2 as cv
import numpy as np
from tqdm import tqdm
class CombinationType(enum.Enum):
average = "average"
maximum = "maximum"
weights = "weights"
def __str__(self):
return self.value
parser = argparse.ArgumentParser(
description="Combine salience maps from different folder to for a final prediction",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"directories", type=str, nargs="+", help="the directories containing salience maps"
)
parser.add_argument(
"--output_dir",
type=str,
default="./",
help="directory where to store the combined salience maps",
)
parser.add_argument(
"--combination",
type=CombinationType,
default=CombinationType.average,
choices=list(CombinationType),
help="define how the salience maps should be combined",
)
parser.add_argument(
"--weights",
type=float,
nargs="+",
help="the weights used for the weights combination type",
)
# TODO: add weighted combination
args = parser.parse_args()
# validate weights if this combination type is chosen
if args.combination == CombinationType.weights:
if len(args.weights) != len(args.directories):
raise ValueError(
f"Number of weights {args.weights} does not match number of directories {args.directories}"
)
for filename in tqdm(os.listdir(args.directories[0])):
sams = map(lambda _dir: os.path.join(_dir, filename), args.directories)
sams = map(lambda sam_file: cv.imread(sam_file, cv.IMREAD_GRAYSCALE), sams)
sams = map(lambda sam: sam.astype(np.int), sams)
sams = list(sams)
for sam, _dir in zip(sams, args.directories):
if sam is not None:
continue
raise RuntimeError(
f"Could not produce combination for {filename} because it is missing in the directory {_dir}"
)
if args.combination == CombinationType.average:
sam = reduce(lambda x, y: x + y, sams)
sam = sam / len(sams)
elif args.combination == CombinationType.maximum:
sam = reduce(np.maximum, sams)
elif args.combination == CombinationType.weights:
sams = map(lambda t: t[0] * t[1], zip(args.weights, sams))
sam = reduce(lambda x, y: x + y, sams)
else:
raise NotImplementedError(
"CombinationType {args.combination} not yet implemented"
)
sam = sam.astype(np.uint8)
cv.imwrite(os.path.join(args.output_dir, filename), sam)
import argparse
import enum
import os
import cv2 as cv
import numpy as np
from PIL import Image
import torch
import torchvision
from tqdm import tqdm
from stain_detection.dataset.csv import IlluminationChannelDataset
from stain_detection.dataset.split import get_laundry_id, get_illumination_id, read_ids
from stain_detection.dataset.patch_sliding import BoundingBox, Patches
from stain_detection.models import add_args, load_model, models
import stain_detection.util.patterns as patterns_util
class OverlapHandling(enum.Enum):
average = "average"
maximum = "maximum"
def __str__(self):
return self.value
parser = argparse.ArgumentParser(
description="Compute the salience maps on alle images in a directory in a patch-wise fashion",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
# Model
add_args(parser)
# Dataset
parser.add_argument(
"--images_dir",
type=str,
required=True,
help="directory where to find the images to process",
)
parser.add_argument(
"--laundry_ids", type=str, help="id file to filter images based on laundry ids"
)
parser.add_argument(
"--illumination_id",
type=int,
default=1,
help="id file to filter images by their illumination id",
)
parser.add_argument(
"--output_dir",
type=str,
default="./",
help="directory where to store the computed salience maps",
)
parser.add_argument(
"--illuminations_to_channel",
action="store_true",
help="set the dataset to load all three illuminations into their own channel",
)
parser.add_argument(
"--channel_mapping",
default=["r", "g", "b"],
nargs=3,
help="if --illuminations_to_channel=True, define the channel with the three letter r, g, b. The below illumination is mapped to the first letter, the above-front to the second and above-back to the third.",
)
# Processing
parser.add_argument(
"--patch_size",
type=int,
default=512,
help="the size of patches extracted from input images",
)
parser.add_argument(
"--stride",
type=int,
default=512,
help="the stride with which patches are moved over input images",
)
parser.add_argument( # TODO: include into model definition
"--batch_size",
type=int,
default=8,
help="patches are gathered into batches of this size for faster processing",
)
parser.add_argument(
"--overlap_handling",
type=OverlapHandling,
default=OverlapHandling.average,
choices=list(OverlapHandling),
help="if the stride is smaller than the patch size there are overlapping regions - define how these are dealt with",
)
patterns_util.add_args(parser)
args = parser.parse_args()
args.channel_mapping = dict(zip(args.channel_mapping, [0, 1, 2]))
torch.set_grad_enabled(False) # gradients are not required for forward pass
device = torch.device(args.device)
model = load_model(args.model, args.weights, device)
model = model.eval()
files = os.listdir(args.images_dir)
if args.laundry_ids:
ids = read_ids(args.laundry_ids)
files = filter(lambda filename: get_laundry_id(filename) in ids, files)
if args.illumination_id in [0, 1, 2]:
files = filter(
lambda filename: get_illumination_id(filename) == args.illumination_id, files
)
else:
print(f"Ignore invalid illumination id {args.illumination_id}")
files = list(files)
files = patterns_util.filter_files(files, args)
for filename in tqdm(files):
abs_filename = os.path.join(args.images_dir, filename)
if args.illuminations_to_channel:
img = IlluminationChannelDataset.load_as_channel_img(abs_filename, args.channel_mapping)
# normally this conversion is part of the loading done in the dataset,
# because here a utility method is used, the conversion is done by hand
img = cv.cvtColor(img, cv.COLOR_RGB2BGR)
else:
img = cv.imread(abs_filename, cv.IMREAD_COLOR)
patches = Patches(img, args.patch_size, args.stride)
patches = list(patches)
sam = np.zeros(img.shape[:2])
# count for each pixel how often it is set to compute an average
# in case of OverlapHandling.average
counts = np.zeros(sam.shape)
_range = range(0, len(patches), args.batch_size)
_range = list(_range)
_range.append(len(patches))
_range = list(zip(_range[:-1], _range[1:]))
for i, j in tqdm(_range):
batch = patches[i:j]
coords = map(lambda b: b[1], batch)
coords = map(
lambda coord: BoundingBox(*coord, args.patch_size, args.patch_size), coords
)
coords = list(coords)
batch_images = map(lambda b: b[0], batch)
batch_images = map(model.preprocess_image, batch_images)
batch_images = list(batch_images)
inputs = torch.stack(batch_images)
inputs = inputs.to(device)
outputs = model(inputs)
salience_maps = model.salience_map(outputs)
salience_maps = salience_maps.cpu().detach().numpy()
salience_maps = (salience_maps * 255).astype(np.uint8)
for _sam, _coord in zip(salience_maps, coords):
_sam = cv.resize(_sam, (args.patch_size, args.patch_size))
if args.overlap_handling == OverlapHandling.average:
counts[_coord.as_slice()] += 1
sam[_coord.as_slice()] += _sam
elif args.overlap_handling == OverlapHandling.maximum:
sam[_coord.as_slice()] = np.maximum(_sam, sam[_coord.as_slice()])
else:
print(f"Overlap Handling {args.overlap_handling} not implemented!")
exit(1)
if args.overlap_handling == OverlapHandling.average:
if (counts == 0).any():
print(f"Could not average because for some region no values were computed!")
exit(1)
sam = sam / counts
sam = sam.astype(np.uint8)
cv.imwrite(os.path.join(args.output_dir, filename), sam)
import os
import random
import cv2 as cv
import numpy as np
import pandas as pd
from PIL import Image
from torch.utils.data import Dataset
def replace_illumination(filename, illumination_id):
dirname = os.path.dirname(filename)
basename = os.path.basename(filename)
basename, ext = os.path.splitext(basename)
split = basename.split("-")
if len(split) > 4:
if illumination_id is None:
split = split[:4]
else:
split[4] = str(illumination_id)
else:
if illumination_id is not None:
split.append(str(illumination_id))
basename = "-".join(split) + ext
return os.path.join(dirname, basename)
class CSVDataset(Dataset):
def __init__(
self,
csv_file,
dir_dataset,
model=None,
augments=[],
illuminations=None,
random_illumination=True,
):
self.table = pd.read_csv(csv_file)
self.dir_dataset = dir_dataset
self.model = model
self.augments = augments
self.illuminations = illuminations
self.random_illumination = random_illumination
if not self.random_illumination:
def replace_illumination_in_row(row, illumination_id):
row["filename"] = replace_illumination(row["filename"], illumination_id)
return row
tables = [
self.table.apply(
lambda row: replace_illumination_in_row(row, _id), axis="columns"
)
for _id in self.illuminations
]
self.table = pd.concat(tables)
def __getitem__(self, index):
row = self.table.iloc[index]
filename = row["filename"]
if self.illuminations is not None and self.random_illumination:
random_index = random.randrange(len(self.illuminations))
filename = replace_illumination(filename, self.illuminations[random_index])
t, l = row["top"], row["left"]
h, w = row["height"], row["width"]
box = (l, t, l + w, t + h)
image = Image.open(os.path.join(self.dir_dataset, "images", filename))
image = image.crop(box).convert("RGB")
label = Image.open(os.path.join(self.dir_dataset, "labels", filename))
label = label.crop(box).convert("L")
for augment in self.augments:
image, label = augment(image, label)
image = np.array(image)
image = cv.cvtColor(image, cv.COLOR_RGB2BGR)
if self.model is not None:
image = self.model.preprocess_image(image)
label = np.array(label)
if self.model is not None:
label = self.model.preprocess_label(label)
return image, label
def __len__(self):
return self.table.shape[0]
class IlluminationChannelDataset:
"""
Dataset which loads patches like a CSV Dataset, but loads image
intensity of each illumination into a separate channel. E.g., the
illumination below becomes the blue channel, the illumination above-
front becomes the green channel and the illumination above-back
the red channel.
"""
@staticmethod
def get_default_channel_mapping():
return {"b": 0, "g": 1, "r": 2}
@staticmethod
def load_as_channel_img(filename, channel_mapping, box=None):
channel_imgs = map(
lambda channel: (
channel,
cv.imread(
replace_illumination(filename, channel_mapping[channel]),
cv.IMREAD_GRAYSCALE,
),
),
["r", "g", "b"],
)
if box is not None:
channel_imgs = map(
lambda channel_img_t: (
channel_img_t[0],
channel_img_t[1][box[1] : box[3], box[0] : box[2]],
),
channel_imgs,
)
channel_imgs = dict(channel_imgs)
image = np.empty((*channel_imgs["r"].shape, 3), dtype=np.uint8)
for i, channel in enumerate(["r", "g", "b"]):
image[:, :, i] = channel_imgs[channel]
return image
def __init__(
self, csv_file, dir_dataset, model=None, augments=[], channel_mapping=None
):
self.table = pd.read_csv(csv_file)
self.dir_dataset = dir_dataset
self.model = model
self.augments = augments
self.channel_mapping = (
self.get_default_channel_mapping()
if channel_mapping is None
else channel_mapping
)
def __getitem__(self, index):
row = self.table.iloc[index]
filename = row["filename"]
t, l = int(row["top"]), int(row["left"])
h, w = int(row["height"]), int(row["width"])
box = (l, t, l + w, t + h)
image = self.load_as_channel_img(
os.path.join(self.dir_dataset, "images", filename),
self.channel_mapping,
box,
)
image = Image.fromarray(image)
_filename = replace_illumination(filename, None)
label = Image.open(os.path.join(self.dir_dataset, "labels_combined", _filename))
label = label.crop(box).convert("L")
for augment in self.augments:
image, label = augment(image, label)
image = np.array(image)
image = cv.cvtColor(image, cv.COLOR_RGB2BGR)
if self.model is not None:
image = self.model.preprocess_image(image)
label = np.array(label)
if self.model is not None:
label = self.model.preprocess_label(label)
return image, label
def __len__(self):
return self.table.shape[0]
import argparse
import datetime
import os
import random
import cv2 as cv
import numpy as np
import pandas as pd
from tqdm import tqdm
from stain_detection.dataset.split import (
get_laundry_id,
get_illumination_id,
read_ids,
get_side,
)
from stain_detection.measures.util import compute_connected_components, BoundingBox
import stain_detection.util.patterns as patterns_util
parser = argparse.ArgumentParser(
"Create a patch dataset with varying patch sizes",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--labels_dir", type=str, required=True, help="directory of label files"
)
parser.add_argument(
"--laundry_ids",
type=str,
help="optional file of laundry ids used to filter the files",
)
parser.add_argument(
"--illumination_id",
type=int,
help="only use labels for a specific illumination id",
)
parser.add_argument(
"--patch_size_min", type=int, default=352, help="the size of extracted patches"
)
parser.add_argument(
"--patch_size_max", type=int, default=352 * 5, help="the size of extracted patches"
)
parser.add_argument(
"--output",