generated from dopt-python/py311
complete initial structuring
This commit is contained in:
parent
d2425f194d
commit
ee328fc0bc
37
src/dopt_sensor_anomalies/_find_paths.py
Normal file
37
src/dopt_sensor_anomalies/_find_paths.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import dopt_basics.io
|
||||||
|
|
||||||
|
from dopt_sensor_anomalies import types as t
|
||||||
|
from dopt_sensor_anomalies.constants import LIB_ROOT_PATH, MODEL_FOLDER_NAME, STOP_FOLDER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_folder() -> Path:
|
||||||
|
path_found = dopt_basics.io.search_folder_path(
|
||||||
|
starting_path=LIB_ROOT_PATH, stop_folder_name=STOP_FOLDER_NAME
|
||||||
|
)
|
||||||
|
if path_found is None:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"The model folder was not found in the application's root directory."
|
||||||
|
)
|
||||||
|
|
||||||
|
return path_found / MODEL_FOLDER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def get_detection_models(model_folder: Path) -> t.DetectionModels:
|
||||||
|
left_model_search = tuple(model_folder.glob("*left_hand_side*.pth"))
|
||||||
|
if not left_model_search:
|
||||||
|
raise ValueError("No model for the left hand side found.")
|
||||||
|
if len(left_model_search) > 1:
|
||||||
|
raise ValueError("Too many models for the left hand side found.")
|
||||||
|
|
||||||
|
right_model_search = tuple(model_folder.glob("*right_hand_side*.pth"))
|
||||||
|
if not right_model_search:
|
||||||
|
raise ValueError("No model for the right hand side found.")
|
||||||
|
elif len(right_model_search) > 1:
|
||||||
|
raise ValueError("Too many models for the right hand side found.")
|
||||||
|
|
||||||
|
left_model = left_model_search[0]
|
||||||
|
right_model = right_model_search[0]
|
||||||
|
|
||||||
|
return t.DetectionModels(left=left_model, right=right_model)
|
||||||
@ -1,5 +1,11 @@
|
|||||||
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
|
LIB_ROOT_PATH: Final[Path] = Path(__file__).parent
|
||||||
|
STOP_FOLDER_NAME: Final[str] = "python"
|
||||||
|
MODEL_FOLDER_NAME: Final[str] = "models"
|
||||||
|
|
||||||
|
# TODO: remove comment
|
||||||
THRESHOLD_BW: Final[int] = 63 # threshold to distringuish black (electrodes) and white areas
|
THRESHOLD_BW: Final[int] = 63 # threshold to distringuish black (electrodes) and white areas
|
||||||
# model_path = [
|
# model_path = [
|
||||||
# r"C:\Users\demon\Documents\EKF\Modelle\patchcore_model_links.pth",
|
# r"C:\Users\demon\Documents\EKF\Modelle\patchcore_model_links.pth",
|
||||||
@ -8,3 +14,8 @@ THRESHOLD_BW: Final[int] = 63 # threshold to distringuish black (electrodes) an
|
|||||||
BACKBONE: Final[str] = "resnet18" # parameters for AI model
|
BACKBONE: Final[str] = "resnet18" # parameters for AI model
|
||||||
LAYERS: Final[tuple[str, str]] = ("layer1", "layer2")
|
LAYERS: Final[tuple[str, str]] = ("layer1", "layer2")
|
||||||
RATIO: Final[float] = 0.05
|
RATIO: Final[float] = 0.05
|
||||||
|
|
||||||
|
NUM_VALID_ELECTRODES: Final[int] = 6
|
||||||
|
# TODO: Remove?
|
||||||
|
# CONTOUR_EXPORT_FILENAME_SUFFIX: Final[str] = "_all_contours"
|
||||||
|
HEATMAP_FILENAME_SUFFIX: Final[str] = "_Heatmap"
|
||||||
|
|||||||
@ -1,80 +1,94 @@
|
|||||||
import csv
|
import csv
|
||||||
from os import path
|
from os import path
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
# Image.MAX_IMAGE_PIXELS = None
|
# Image.MAX_IMAGE_PIXELS = None
|
||||||
import cv2
|
import cv2
|
||||||
|
import imutils
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from anomalib.data import Folder
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
import torch
|
||||||
from anomalib.engine import Engine
|
from anomalib.engine import Engine
|
||||||
from anomalib.metrics import AUROC, F1Score
|
|
||||||
from anomalib.models import Patchcore
|
from anomalib.models import Patchcore
|
||||||
from imutils import contours, grab_contours, is_cv2, perspective
|
from imutils import contours, perspective
|
||||||
from numpy import all, array, linalg
|
|
||||||
from numpy import max as npmax
|
|
||||||
from numpy import min as npmin
|
|
||||||
from numpy import sum as npsum
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from scipy.spatial import distance as dist
|
from scipy.spatial import distance as dist
|
||||||
from torch import as_tensor, cuda, device, float32, load, no_grad
|
|
||||||
from torchvision.transforms.v2.functional import to_dtype, to_image
|
from torchvision.transforms.v2.functional import to_dtype, to_image
|
||||||
|
|
||||||
|
import dopt_sensor_anomalies._find_paths
|
||||||
from dopt_sensor_anomalies import constants as const
|
from dopt_sensor_anomalies import constants as const
|
||||||
|
from dopt_sensor_anomalies import errors
|
||||||
|
from dopt_sensor_anomalies import types as t
|
||||||
|
|
||||||
# input parameters: user-defined
|
# input parameters: user-defined
|
||||||
file_path = r"C:\Users\demon\Documents\EKF\Analyse_fuer_Florian\bild2.bmp"
|
file_path: Path = Path(r"C:\Users\demon\Documents\EKF\Analyse_fuer_Florian\bild2.bmp")
|
||||||
pixelsPerMetricX = 0.251
|
pixels_per_metric_X: float = 0.251
|
||||||
pixelsPerMetricY = 0.251
|
pixels_per_metric_Y: float = 0.251
|
||||||
|
|
||||||
|
|
||||||
# internal parameters - configuration
|
|
||||||
schwellwert = 63 # threshold to distinguish black (electrodes) and white areas
|
|
||||||
model_path = [
|
|
||||||
r"C:\Users\demon\Documents\EKF\Modelle\patchcore_model_links.pth",
|
|
||||||
r"C:\Users\demon\Documents\EKF\Modelle\patchcore_model_rechts.pth",
|
|
||||||
] # path to anomaly detection models
|
|
||||||
|
|
||||||
|
|
||||||
# measuring
|
# measuring
|
||||||
def midpoint(ptA, ptB):
|
def midpoint(ptA: npt.NDArray, ptB: npt.NDArray) -> tuple[float, float]:
|
||||||
# ----------------------------
|
"""to identify the midpoint of a 2D area
|
||||||
# To identify the midpoint of a 2D area
|
|
||||||
# Input:
|
|
||||||
# ptA (numpy.ndarray of shape (2, )): tuple of coordinates x, y
|
|
||||||
# ptB (numpy.ndarray of shape (2, )): tuple of coordinates x, y
|
|
||||||
# Output (tuple (float, float)):
|
|
||||||
# tuple of midpoint coordinates
|
|
||||||
# ----------------------------
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ptA : npt.NDArray
|
||||||
|
tuple of coordinates x, y; shape (2, )
|
||||||
|
ptB : npt.NDArray
|
||||||
|
tuple of coordinates x, y; shape (2, )
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple[float, float]
|
||||||
|
tuple of midpoint coordinates
|
||||||
|
"""
|
||||||
return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)
|
return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)
|
||||||
|
|
||||||
|
|
||||||
def check_box_redundancy(box1, box2, tolerance=5):
|
def check_box_redundancy(
|
||||||
# ----------------------------
|
box1: t.Box,
|
||||||
# To check if bounding box has already been identified and is just a redundant one
|
box2: t.Box,
|
||||||
# Input:
|
tolerance: float = 5.0,
|
||||||
# box1 (tuple(float, float), (float, float), float)): tuple of box values: ((center_x, center_y), (width, height), angle)
|
) -> bool:
|
||||||
# box2 (tuple(float, float), (float, float), float)): tuple of box values: ((center_x, center_y), (width, height), angle)
|
"""to check if bounding box has already been identified and is just a redundant one
|
||||||
# tolerance (float): distance threshold for width and height
|
|
||||||
# Output (Boole):
|
|
||||||
# redundancy evaluation
|
|
||||||
# ----------------------------
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
box1 : t.Box
|
||||||
|
tuple of box values: ((center_x, center_y), (width, height), angle)
|
||||||
|
box2 : t.Box
|
||||||
|
tuple of box values: ((center_x, center_y), (width, height), angle)
|
||||||
|
tolerance : float, optional
|
||||||
|
distance threshold for width and height, by default 5.0
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
redundancy evaluation
|
||||||
|
"""
|
||||||
# unpack the boxes
|
# unpack the boxes
|
||||||
(c1, s1, a1) = box1
|
c1, s1, _ = box1
|
||||||
(c2, s2, a2) = box2
|
c2, s2, _ = box2
|
||||||
|
# sort width and height such that (w, h) == (h, w) is treated the same
|
||||||
# sort width and height such that (w, h) == (h, w) is treated the same (might have been recognized in different orders)
|
# (might have been recognized in different orders)
|
||||||
s1 = sorted(s1)
|
s1 = sorted(s1)
|
||||||
s2 = sorted(s2)
|
s2 = sorted(s2)
|
||||||
|
|
||||||
center_dist = linalg.norm(array(c1) - array(c2))
|
center_dist = cast(float, np.linalg.norm(np.array(c1) - np.array(c2)))
|
||||||
size_diff = linalg.norm(array(s1) - array(s2))
|
size_diff = cast(float, np.linalg.norm(np.array(s1) - np.array(s2)))
|
||||||
|
|
||||||
return center_dist < tolerance and size_diff < tolerance
|
return center_dist < tolerance and size_diff < tolerance
|
||||||
|
|
||||||
|
|
||||||
def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
# ** main function
|
||||||
|
def measure_length(
|
||||||
|
file_path: Path,
|
||||||
|
pixels_per_metric_X: float,
|
||||||
|
pixels_per_metric_Y: float,
|
||||||
|
) -> tuple[list[str | int], t.SensorImages]:
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# To identify the midpoint of a 2D area
|
# To identify the midpoint of a 2D area
|
||||||
# Input:
|
# Input:
|
||||||
@ -86,53 +100,39 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
# image of left sensor
|
# image of left sensor
|
||||||
# image of right sensor
|
# image of right sensor
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
file_stem = file_path.stem
|
||||||
file = path.basename(file_path)
|
folder_path = file_path.parent
|
||||||
# extract file name and ending separately
|
data_csv: list[str | int] = []
|
||||||
name, endung = path.splitext(file)
|
image = cv2.imread(str(file_path))
|
||||||
# extract folder path
|
|
||||||
folder_path = path.dirname(file_path)
|
|
||||||
|
|
||||||
# for data output
|
|
||||||
data_csv = []
|
|
||||||
|
|
||||||
# read
|
|
||||||
image = cv2.imread(file_path)
|
|
||||||
|
|
||||||
# check if image was read
|
|
||||||
if image is None:
|
if image is None:
|
||||||
error = "error: no image read"
|
raise errors.ImageNotReadError(f"Image could not be read from: >{file_path}<")
|
||||||
return
|
|
||||||
|
|
||||||
# crop image
|
|
||||||
cropped = image[500:1500, 100 : image.shape[1] - 100]
|
cropped = image[500:1500, 100 : image.shape[1] - 100]
|
||||||
|
|
||||||
# store original image for later output
|
|
||||||
orig = cropped.copy()
|
orig = cropped.copy()
|
||||||
height, width = cropped.shape[0], cropped.shape[1]
|
# TODO: check removal
|
||||||
|
# height, width = cropped.shape[0], cropped.shape[1]
|
||||||
|
|
||||||
# change colours in the image to black and white
|
# change colours in the image to black and white
|
||||||
gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
|
||||||
_, binary = cv2.threshold(gray, const.THRESHOLD_BW, 255, cv2.THRESH_BINARY)
|
_, binary = cv2.threshold(gray, const.THRESHOLD_BW, 255, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
# perform edge detection, identify rectangular shapes
|
# perform edge detection, identify rectangular shapes
|
||||||
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
|
||||||
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
|
closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
|
||||||
edged = cv2.Canny(closed, 50, 100)
|
edged = cv2.Canny(closed, 50, 100)
|
||||||
|
|
||||||
# find contours in the edge map
|
# find contours in the edge map
|
||||||
cnts = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
cnts = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
cnts = grab_contours(cnts)
|
cnts = imutils.grab_contours(cnts)
|
||||||
|
|
||||||
if cnts is None:
|
if cnts is None:
|
||||||
print(f"{file}: offenbar nichts gefunden")
|
raise errors.ContourCalculationError(
|
||||||
return None, None
|
"No contours were found in the provided image. Can not continue analysis."
|
||||||
|
)
|
||||||
|
|
||||||
# sort the contours from left to right (i.e., use x coordinates)
|
# sort the contours from left to right (i.e., use x coordinates)
|
||||||
cnts, _ = contours.sort_contours(cnts)
|
cnts, _ = contours.sort_contours(cnts)
|
||||||
|
# TODO: remove???
|
||||||
# bounding_boxes = list(set([cv2.boundingRect(c) for c in cnts]))
|
# bounding_boxes = list(set([cv2.boundingRect(c) for c in cnts]))
|
||||||
# cnts = [c for _, c in sorted(zip(bounding_boxes, cnts), key=lambda b: b[0][0])]
|
# cnts = [c for _, c in sorted(zip(bounding_boxes, cnts), key=lambda b: b[0][0])]
|
||||||
|
|
||||||
# min_area = 1000 # adjust as needed
|
# min_area = 1000 # adjust as needed
|
||||||
# filtered_cnts = [c for c in cnts if cv2.contourArea(c) > min_area]
|
# filtered_cnts = [c for c in cnts if cv2.contourArea(c) > min_area]
|
||||||
|
|
||||||
@ -140,27 +140,32 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
# get x coordinates of bounding boxes
|
# get x coordinates of bounding boxes
|
||||||
x_coords = [cv2.boundingRect(c)[0] for c in cnts]
|
x_coords = [cv2.boundingRect(c)[0] for c in cnts]
|
||||||
# check if x coordinates are sorted in increasing order
|
# check if x coordinates are sorted in increasing order
|
||||||
is_sorted = all(x1 <= x2 for x1, x2 in zip(x_coords, x_coords[1:]))
|
is_sorted = np.all(x1 <= x2 for x1, x2 in zip(x_coords, x_coords[1:])) # type: ignore
|
||||||
if not is_sorted:
|
if not is_sorted:
|
||||||
error = (
|
raise errors.ContourCalculationError(
|
||||||
"contour detection not valid: contours are not properly sorted from left to right"
|
"Contour detection not valid: contours are not "
|
||||||
|
"properly sorted from left to right."
|
||||||
)
|
)
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# TODO: Remove??
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
output_image = gray.copy()
|
output_image = gray.copy()
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
|
##################################################################
|
||||||
|
|
||||||
# to store only electrodes contours and nothing redundant
|
# to store only electrodes contours and nothing redundant
|
||||||
accepted_boxes = []
|
accepted_boxes: list[t.Box] = []
|
||||||
filtered_cnts = []
|
filtered_cnts: list[Any] = []
|
||||||
|
|
||||||
# loop over the contours individually
|
# loop over the contours individually
|
||||||
for c in cnts:
|
for c in cnts:
|
||||||
# compute the rotated bounding box of the contour
|
# compute the rotated bounding box of the contour
|
||||||
rbox = cv2.minAreaRect(c)
|
rbox = cast(t.Box, cv2.minAreaRect(c))
|
||||||
box = cv2.cv.BoxPoints(rbox) if is_cv2() else cv2.boxPoints(rbox)
|
# !! should only be newer OpenCV versions
|
||||||
box = array(box, dtype="int")
|
# box = cv2.cv.BoxPoints(rbox) if is_cv2() else cv2.boxPoints(rbox)
|
||||||
|
box = cv2.boxPoints(rbox)
|
||||||
|
box = np.array(box, dtype="int")
|
||||||
# order the points in the contour in top-left, top-right, bottom-right, and bottom-left
|
# order the points in the contour in top-left, top-right, bottom-right, and bottom-left
|
||||||
box = perspective.order_points(box)
|
box = perspective.order_points(box)
|
||||||
|
|
||||||
@ -193,8 +198,8 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
filtered_cnts.append(c)
|
filtered_cnts.append(c)
|
||||||
|
|
||||||
# compute the size of the electrode object
|
# compute the size of the electrode object
|
||||||
dimA = dA / pixelsPerMetricY # y
|
dimA = dA / pixels_per_metric_Y # y
|
||||||
dimB = dB / pixelsPerMetricX # x
|
dimB = dB / pixels_per_metric_X # x
|
||||||
|
|
||||||
data_csv.extend(
|
data_csv.extend(
|
||||||
[
|
[
|
||||||
@ -204,6 +209,8 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# TODO: Remove??
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
count = 1
|
count = 1
|
||||||
# loop over the original points and draw everything
|
# loop over the original points and draw everything
|
||||||
@ -252,25 +259,28 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
# cv2.imwrite(path.join(folder_path, f'{name}_contour_{count}.png'), output_image)
|
# cv2.imwrite(path.join(folder_path, f'{name}_contour_{count}.png'), output_image)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
cv2.imwrite(path.join(folder_path, f"{name}_all_contours.png"), output_image)
|
cv2.imwrite(str(folder_path / f"{file_stem}_all_contours.png"), output_image)
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
|
##################################################################
|
||||||
|
|
||||||
if not filtered_cnts:
|
if not filtered_cnts:
|
||||||
error = "contour detection not valid: no contours recognized"
|
raise errors.ContourCalculationError(
|
||||||
return None, None
|
"Contour detection not valid: no contours recognized"
|
||||||
|
)
|
||||||
|
|
||||||
# if incorrect number of electrodes has been identified
|
# if incorrect number of electrodes has been identified
|
||||||
if len(filtered_cnts) != 6:
|
num_contours = len(filtered_cnts)
|
||||||
print("falsche Anzahl an Elektroden identifiziert", len(filtered_cnts))
|
if num_contours != const.NUM_VALID_ELECTRODES:
|
||||||
data_csv = [-1] * 6
|
raise errors.InvalidElectrodeCount(
|
||||||
return data_csv, None
|
f"Number of counted electroedes does not match the "
|
||||||
|
f"expected value: count = {num_contours}, expected = {const.NUM_VALID_ELECTRODES}"
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
|
||||||
# identify left and right sensor areas
|
# identify left and right sensor areas
|
||||||
x_min = min(npmin(c[:, 0, 0]) for c in filtered_cnts) - 20
|
x_min = min(np.min(c[:, 0, 0]) for c in filtered_cnts) - 20
|
||||||
x_max = max(npmax(c[:, 0, 0]) for c in filtered_cnts) + 20
|
x_max = max(np.max(c[:, 0, 0]) for c in filtered_cnts) + 20
|
||||||
y_min = min(npmin(c[:, 0, 1]) for c in filtered_cnts) - 20
|
y_min = min(np.min(c[:, 0, 1]) for c in filtered_cnts) - 20
|
||||||
y_max = max(npmax(c[:, 0, 1]) for c in filtered_cnts) + 20
|
y_max = max(np.max(c[:, 0, 1]) for c in filtered_cnts) + 20
|
||||||
|
|
||||||
rightmost_x_third = max(filtered_cnts[2][:, 0, 0])
|
rightmost_x_third = max(filtered_cnts[2][:, 0, 0])
|
||||||
leftmost_x_fourth = min(filtered_cnts[3][:, 0, 0])
|
leftmost_x_fourth = min(filtered_cnts[3][:, 0, 0])
|
||||||
@ -280,20 +290,27 @@ def measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY):
|
|||||||
cropped_sensor_left = orig[y_min:y_max, x_min:x_middle]
|
cropped_sensor_left = orig[y_min:y_max, x_min:x_middle]
|
||||||
cropped_sensor_right = orig[y_min:y_max, x_middle:x_max]
|
cropped_sensor_right = orig[y_min:y_max, x_middle:x_max]
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# TODO: Remove??
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
# save the cropped images for left and right sensor
|
# save the cropped images for left and right sensor
|
||||||
try:
|
try:
|
||||||
cv2.imwrite(path.join(folder_path, f"{name}_left.png"), cropped_sensor_left)
|
cv2.imwrite(path.join(folder_path, f"{file_stem}_left.png"), cropped_sensor_left)
|
||||||
cv2.imwrite(path.join(folder_path, f"{name}_right.png"), cropped_sensor_right)
|
cv2.imwrite(path.join(folder_path, f"{file_stem}_right.png"), cropped_sensor_right)
|
||||||
except:
|
except Exception as err:
|
||||||
print("not possible")
|
print(f"not possible: Error: {err}")
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
|
##################################################################
|
||||||
|
|
||||||
return data_csv, (cropped_sensor_left, cropped_sensor_right)
|
return data_csv, t.SensorImages(left=cropped_sensor_left, right=cropped_sensor_right)
|
||||||
|
|
||||||
|
|
||||||
|
# helper function
|
||||||
# anomaly detection
|
# anomaly detection
|
||||||
def infer_image(image, model):
|
def infer_image(
|
||||||
|
image: npt.NDArray,
|
||||||
|
model: Patchcore,
|
||||||
|
) -> t.InferenceResult:
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# To evaluate the image
|
# To evaluate the image
|
||||||
# Input:
|
# Input:
|
||||||
@ -306,20 +323,24 @@ def infer_image(image, model):
|
|||||||
# anomaly_label (bool): anomaly detected (1) or not (0)
|
# anomaly_label (bool): anomaly detected (1) or not (0)
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|
||||||
torch_device = device("cuda" if cuda.is_available() else "cpu")
|
torch_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
model.to(torch_device)
|
model.to(torch_device)
|
||||||
|
|
||||||
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # this is optional
|
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # this is optional
|
||||||
pil_image = Image.fromarray(image_rgb)
|
pil_image = Image.fromarray(image_rgb)
|
||||||
image = pil_image.convert("RGB")
|
pil_image = pil_image.convert("RGB")
|
||||||
input_tensor = (
|
input_tensor = (
|
||||||
to_dtype(to_image(image), float32, scale=True) if as_tensor else array(image) / 255.0
|
to_dtype(to_image(pil_image), torch.float32, scale=True)
|
||||||
|
if torch.as_tensor # ?? Question: Wie passt diese Funktion hier rein?
|
||||||
|
# ?? Konvertiert, aber wird zur Evaluation der Aussage genutzt (sollte immer wahr sein?)
|
||||||
|
else np.array(pil_image) / 255.0
|
||||||
)
|
)
|
||||||
|
# ?? Ist das immer ein Torch-Tensor? Falls nicht, müsste die Methode geändert werden
|
||||||
input_tensor = input_tensor.unsqueeze(0)
|
input_tensor = input_tensor.unsqueeze(0)
|
||||||
input_tensor = input_tensor.to(torch_device)
|
input_tensor = input_tensor.to(torch_device)
|
||||||
|
|
||||||
model.eval()
|
model.eval()
|
||||||
with no_grad():
|
with torch.no_grad():
|
||||||
output = model(input_tensor)
|
output = model(input_tensor)
|
||||||
|
|
||||||
anomaly_score = output.pred_score.item()
|
anomaly_score = output.pred_score.item()
|
||||||
@ -327,13 +348,19 @@ def infer_image(image, model):
|
|||||||
anomaly_map = output.anomaly_map.squeeze().cpu().numpy()
|
anomaly_map = output.anomaly_map.squeeze().cpu().numpy()
|
||||||
|
|
||||||
# resize heatmap to original image size
|
# resize heatmap to original image size
|
||||||
img_np = array(image)
|
img_np = np.array(pil_image)
|
||||||
anomaly_map_resized = cv2.resize(anomaly_map, (img_np.shape[1], img_np.shape[0]))
|
anomaly_map_resized = cv2.resize(anomaly_map, (img_np.shape[1], img_np.shape[0]))
|
||||||
|
|
||||||
return img_np, anomaly_map_resized, anomaly_score, anomaly_label
|
return img_np, anomaly_map_resized, anomaly_score, anomaly_label
|
||||||
|
|
||||||
|
|
||||||
def anomaly_detection(file_path, data_csv, sensor_images):
|
# ** main function
|
||||||
|
def anomaly_detection(
|
||||||
|
file_path: Path,
|
||||||
|
detection_models: t.DetectionModels,
|
||||||
|
data_csv: list[str | int],
|
||||||
|
sensor_images: t.SensorImages,
|
||||||
|
) -> None:
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# To load the model, call function for anomaly detection and store the results
|
# To load the model, call function for anomaly detection and store the results
|
||||||
# Input:
|
# Input:
|
||||||
@ -342,38 +369,37 @@ def anomaly_detection(file_path, data_csv, sensor_images):
|
|||||||
# Output:
|
# Output:
|
||||||
# none
|
# none
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
file = path.basename(file_path)
|
file_stem = file_path.stem
|
||||||
# extract file name and ending separately
|
folder_path = file_path.parent
|
||||||
name, endung = path.splitext(file)
|
|
||||||
# extract folder path
|
|
||||||
folder_path = path.dirname(file_path)
|
|
||||||
|
|
||||||
# reconstruct the model and initialize the engine
|
# reconstruct the model and initialize the engine
|
||||||
model = Patchcore(
|
model = Patchcore(
|
||||||
backbone=const.BACKBONE, layers=const.LAYERS, coreset_sampling_ratio=const.RATIO
|
backbone=const.BACKBONE, layers=const.LAYERS, coreset_sampling_ratio=const.RATIO
|
||||||
)
|
)
|
||||||
|
# ?? benötigt? Wird nicht genutzt
|
||||||
engine = Engine()
|
engine = Engine()
|
||||||
|
|
||||||
# preparation for plot
|
# preparation for plot
|
||||||
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
|
_, axes = plt.subplots(1, 2, figsize=(12, 6))
|
||||||
|
|
||||||
# loop over left and right sensor
|
# loop over left and right sensor
|
||||||
for i, image in enumerate(sensor_images):
|
for i, (side, image) in enumerate(sensor_images.items()):
|
||||||
# load the model
|
# Ich habe die Modellpfade als Funktionsparameter hinzugefügt
|
||||||
checkpoint = load(model_path[i])
|
image = cast(npt.NDArray, image)
|
||||||
|
checkpoint = torch.load(detection_models[side])
|
||||||
model.load_state_dict(checkpoint["model_state_dict"])
|
model.load_state_dict(checkpoint["model_state_dict"])
|
||||||
|
|
||||||
# evaluate image
|
_, anomaly_map_resized, score, label = infer_image(image, model)
|
||||||
img_np, anomaly_map_resized, score, label = infer_image(image, model)
|
|
||||||
|
|
||||||
|
##################################################################
|
||||||
|
# TODO: Remove??
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
print(score)
|
print(score)
|
||||||
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
# ---------------------------------------- just for internal evaluation ---------------------------------------
|
||||||
|
##################################################################
|
||||||
|
|
||||||
# add result to data_csv
|
|
||||||
data_csv.extend([int(label)])
|
data_csv.extend([int(label)])
|
||||||
|
|
||||||
# store heatmap
|
|
||||||
ax = axes[i]
|
ax = axes[i]
|
||||||
ax.axis("off")
|
ax.axis("off")
|
||||||
ax.imshow(image, alpha=0.8)
|
ax.imshow(image, alpha=0.8)
|
||||||
@ -381,14 +407,15 @@ def anomaly_detection(file_path, data_csv, sensor_images):
|
|||||||
|
|
||||||
plt.subplots_adjust(wspace=0, hspace=0)
|
plt.subplots_adjust(wspace=0, hspace=0)
|
||||||
plt.savefig(
|
plt.savefig(
|
||||||
path.join(folder_path, f"{name}_Heatmap.png"), bbox_inches="tight", pad_inches=0
|
(folder_path / f"{file_stem}{const.HEATMAP_FILENAME_SUFFIX}.png"),
|
||||||
|
bbox_inches="tight",
|
||||||
|
pad_inches=0,
|
||||||
)
|
)
|
||||||
plt.close()
|
plt.close()
|
||||||
|
|
||||||
# save csv file
|
|
||||||
df = DataFrame([data_csv])
|
df = DataFrame([data_csv])
|
||||||
df.to_csv(
|
df.to_csv(
|
||||||
path.join(folder_path, f"{name}.csv"),
|
(folder_path / f"{file_stem}.csv"),
|
||||||
mode="w",
|
mode="w",
|
||||||
index=False,
|
index=False,
|
||||||
header=False,
|
header=False,
|
||||||
@ -396,9 +423,27 @@ def anomaly_detection(file_path, data_csv, sensor_images):
|
|||||||
sep=";",
|
sep=";",
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
def pipeline(
|
||||||
|
user_file_path: str,
|
||||||
|
pixels_per_metric_X: float,
|
||||||
|
pixels_per_metric_Y: float,
|
||||||
|
) -> None:
|
||||||
|
file_path = Path(user_file_path)
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError("The provided path seems not to exist")
|
||||||
|
|
||||||
data_csv, sensors = measure_length(file_path, pixelsPerMetricX, pixelsPerMetricY)
|
MODEL_FOLDER: Final[Path] = dopt_sensor_anomalies._find_paths.get_model_folder()
|
||||||
|
DETECTION_MODELS: Final[t.DetectionModels] = (
|
||||||
|
dopt_sensor_anomalies._find_paths.get_detection_models(MODEL_FOLDER)
|
||||||
|
)
|
||||||
|
|
||||||
anomaly_detection(file_path, data_csv, sensors)
|
data_csv, sensor_images = measure_length(
|
||||||
|
file_path, pixels_per_metric_X, pixels_per_metric_Y
|
||||||
|
)
|
||||||
|
anomaly_detection(
|
||||||
|
file_path=file_path,
|
||||||
|
detection_models=DETECTION_MODELS,
|
||||||
|
data_csv=data_csv,
|
||||||
|
sensor_images=sensor_images,
|
||||||
|
)
|
||||||
|
|||||||
10
src/dopt_sensor_anomalies/errors.py
Normal file
10
src/dopt_sensor_anomalies/errors.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class ImageNotReadError(Exception):
|
||||||
|
"""thrown if image was not read successfully"""
|
||||||
|
|
||||||
|
|
||||||
|
class ContourCalculationError(Exception):
|
||||||
|
"""thrown if contour detection was not successful"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidElectrodeCount(Exception):
|
||||||
|
"""thrown if the number of electrodes does not match the expected value"""
|
||||||
18
src/dopt_sensor_anomalies/types.py
Normal file
18
src/dopt_sensor_anomalies/types.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import dataclasses as dc
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TypeAlias, TypedDict
|
||||||
|
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
Box: TypeAlias = tuple[tuple[float, float], tuple[float, float], float]
|
||||||
|
InferenceResult: TypeAlias = tuple[npt.NDArray, npt.NDArray, float, bool]
|
||||||
|
|
||||||
|
|
||||||
|
class SensorImages(TypedDict):
|
||||||
|
left: npt.NDArray
|
||||||
|
right: npt.NDArray
|
||||||
|
|
||||||
|
|
||||||
|
class DetectionModels(TypedDict):
|
||||||
|
left: Path
|
||||||
|
right: Path
|
||||||
102
tests/test_find_paths.py
Normal file
102
tests/test_find_paths.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from dopt_sensor_anomalies import _find_paths
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def setup_temp_dir(tmp_path_factory):
|
||||||
|
tmp_dir = tmp_path_factory.mktemp("root")
|
||||||
|
folder_structure = "lib/folder"
|
||||||
|
pth = tmp_dir / folder_structure
|
||||||
|
pth.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with patch("dopt_sensor_anomalies._find_paths.LIB_ROOT_PATH", pth):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def temp_model_folder_empty(tmp_path_factory) -> Path:
|
||||||
|
return tmp_path_factory.mktemp("empty")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def temp_model_folder_full(tmp_path_factory) -> Path:
|
||||||
|
folder = tmp_path_factory.mktemp("full")
|
||||||
|
left_hand_model = folder / "this_file_contains_the_left_hand_side_model.pth"
|
||||||
|
right_hand_model = folder / "this_file_contains_the_right_hand_side_model.pth"
|
||||||
|
left_hand_model.touch()
|
||||||
|
right_hand_model.touch()
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def temp_model_folder_only_left(tmp_path_factory) -> Path:
|
||||||
|
folder = tmp_path_factory.mktemp("only_left")
|
||||||
|
left_hand_model = folder / "this_file_contains_the_left_hand_side_model.pth"
|
||||||
|
left_hand_model.touch()
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def temp_model_folder_only_right(tmp_path_factory) -> Path:
|
||||||
|
folder = tmp_path_factory.mktemp("only_right")
|
||||||
|
right_hand_model = folder / "this_file_contains_the_right_hand_side_model.pth"
|
||||||
|
right_hand_model.touch()
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
|
@patch("dopt_sensor_anomalies._find_paths.STOP_FOLDER_NAME", "not-found")
|
||||||
|
def test_get_model_folder_Fail_NotFound():
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
_ = _find_paths.get_model_folder()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("dopt_sensor_anomalies._find_paths.STOP_FOLDER_NAME", "lib")
|
||||||
|
def test_get_model_folder_Success():
|
||||||
|
ret = _find_paths.get_model_folder()
|
||||||
|
assert ret is not None
|
||||||
|
assert ret.name == _find_paths.MODEL_FOLDER_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_FailEmptyDir(temp_model_folder_empty):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = _find_paths.get_detection_models(temp_model_folder_empty)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_FailOnlyLeft(temp_model_folder_only_left):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = _find_paths.get_detection_models(temp_model_folder_only_left)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_FailOnlyRight(temp_model_folder_only_right):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = _find_paths.get_detection_models(temp_model_folder_only_right)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_FailTooManyLeft(temp_model_folder_full):
|
||||||
|
right_hand_model = (
|
||||||
|
temp_model_folder_full / "this_file_contains_the_left_hand_side_model2.pth"
|
||||||
|
)
|
||||||
|
right_hand_model.touch()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = _find_paths.get_detection_models(temp_model_folder_full)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_FailTooManyRight(temp_model_folder_full):
|
||||||
|
right_hand_model = (
|
||||||
|
temp_model_folder_full / "this_file_contains_the_right_hand_side_model2.pth"
|
||||||
|
)
|
||||||
|
right_hand_model.touch()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = _find_paths.get_detection_models(temp_model_folder_full)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_detection_models_Success(temp_model_folder_full):
|
||||||
|
models = _find_paths.get_detection_models(temp_model_folder_full)
|
||||||
|
assert "left_hand" in models["left"].name
|
||||||
|
assert "right_hand" in models["right"].name
|
||||||
Loading…
x
Reference in New Issue
Block a user