import numpy as np import cv2 import time import logging # Set up logging logging.basicConfig(level=logging.INFO) Logger = logging.getLogger(__name__) def MergeBoxes(Boxes, Padding=5): if len(Boxes) <= 1: return Boxes MergedOccurred = True while MergedOccurred: MergedOccurred = False NewBoxes = [] Boxes.sort(key=lambda b: b[0]) Used = [False] * len(Boxes) for Index in range(len(Boxes)): if Used[Index]: continue CurrentBox = list(Boxes[Index]) Used[Index] = True for J in range(Index + 1, len(Boxes)): if Used[J]: continue NextBox = Boxes[J] OverlapX = max(CurrentBox[0], NextBox[0]) <= min(CurrentBox[0] + CurrentBox[2], NextBox[0] + NextBox[2]) + Padding OverlapY = max(CurrentBox[1], NextBox[1]) <= min(CurrentBox[1] + CurrentBox[3], NextBox[1] + NextBox[3]) + Padding if OverlapX and OverlapY: NewX = min(CurrentBox[0], NextBox[0]) NewY = min(CurrentBox[1], NextBox[1]) NewW = max(CurrentBox[0] + CurrentBox[2], NextBox[0] + NextBox[2]) - NewX NewH = max(CurrentBox[1] + CurrentBox[3], NextBox[1] + NextBox[3]) - NewY CurrentBox = [NewX, NewY, NewW, NewH] Used[J] = True MergedOccurred = True NewBoxes.append(tuple(CurrentBox)) Boxes = NewBoxes return Boxes def GetChangeMask(Image1, Image2, Threshold=25, MinArea=100): if Image1.shape != Image2.shape: Logger.warning(f'Image shapes differ: {Image1.shape} vs {Image2.shape}. Resizing Image2.') Image2 = cv2.resize(Image2, (Image1.shape[1], Image1.shape[0])) Gray1 = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY) Gray2 = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY) Blur1 = cv2.GaussianBlur(Gray1, (5, 5), 0) Blur2 = cv2.GaussianBlur(Gray2, (5, 5), 0) DiffFrame = cv2.absdiff(Blur1, Blur2) _, ThresholdCalc = cv2.threshold(DiffFrame, Threshold, 255, cv2.THRESH_BINARY) Kernel = np.ones((5, 5), np.uint8) DilatedThreshold = cv2.dilate(ThresholdCalc, Kernel, iterations=2) Contours, _ = cv2.findContours(DilatedThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) OutputMask = np.zeros_like(DilatedThreshold) ValidContours = 0 if Contours: for Contour in Contours: if cv2.contourArea(Contour) > MinArea: cv2.drawContours(OutputMask, [Contour], -1, 255, -1) # type: ignore ValidContours +=1 Logger.info(f'GetChangeMask: Found {len(Contours)} raw contours, kept {ValidContours} after MinArea filter ({MinArea}).') return OutputMask def VisualizeDifferences(Image1Path, Image2Path, OutputPath, Threshold=25, MinArea=100, OutlineColor=(0, 255, 0), FillColor=(0, 180, 0), FillAlpha=0.3): Logger.info(f'🎨 Visualizing differences between {Image1Path} and {Image2Path}') Image1 = cv2.imread(Image1Path) Image2 = cv2.imread(Image2Path) if Image1 is None or Image2 is None: Logger.error(f'❌ Error loading images for visualization: {Image1Path} or {Image2Path}') return if Image1.shape != Image2.shape: Logger.warning(f'⚠️ Image shapes differ: {Image1.shape} vs {Image2.shape}. Resizing Image2 for visualization.') Image2 = cv2.resize(Image2, (Image1.shape[1], Image1.shape[0])) ChangedMask = GetChangeMask(Image1, Image2, Threshold, MinArea) OutputImage = Image2.copy() Overlay = OutputImage.copy() # Apply fill color to changed areas Overlay[ChangedMask == 255] = FillColor cv2.addWeighted(Overlay, FillAlpha, OutputImage, 1 - FillAlpha, 0, OutputImage) # Find contours of the changed areas to draw outlines Contours, _ = cv2.findContours(ChangedMask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(OutputImage, Contours, -1, OutlineColor, 2) Logger.info(f'🎨 Drew {len(Contours)} difference regions.') try: cv2.imwrite(OutputPath, OutputImage) Logger.info(f'💾 Saved difference visualization to {OutputPath}') except Exception as E: Logger.error(f'❌ Failed to save visualization {OutputPath}: {E}') # --- Function to be used in App.py for upscaling --- def GetChangedRegions(Image1, Image2, Threshold=25, Padding=10, MinArea=100, MergePadding=5): StartTime = time.time() Logger.info('🔄 Comparing images...') if Image1 is None or Image2 is None: Logger.error('❌ Cannot compare None images.') return [] if Image1.shape != Image2.shape: Logger.warning(f'⚠️ Image shapes differ: {Image1.shape} vs {Image2.shape}. Resizing Image2.') Image2 = cv2.resize(Image2, (Image1.shape[1], Image1.shape[0])) Gray1 = cv2.cvtColor(Image1, cv2.COLOR_BGR2GRAY) Gray2 = cv2.cvtColor(Image2, cv2.COLOR_BGR2GRAY) Blur1 = cv2.GaussianBlur(Gray1, (5, 5), 0) Blur2 = cv2.GaussianBlur(Gray2, (5, 5), 0) DiffFrame = cv2.absdiff(Blur1, Blur2) _, ThresholdCalc = cv2.threshold(DiffFrame, Threshold, 255, cv2.THRESH_BINARY) Kernel = np.ones((5, 5), np.uint8) DilatedThreshold = cv2.dilate(ThresholdCalc, Kernel, iterations=2) Contours, _ = cv2.findContours(DilatedThreshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) Logger.info(f'🔎 Found {len(Contours)} raw contours.') BoundingBoxes = [] if Contours: ValidContours = 0 for Contour in Contours: ContourArea = cv2.contourArea(Contour) if ContourArea > MinArea: ValidContours += 1 X, Y, W, H = cv2.boundingRect(Contour) PaddedX = max(0, X - Padding) PaddedY = max(0, Y - Padding) MaxW = Image1.shape[1] - PaddedX MaxH = Image1.shape[0] - PaddedY PaddedW = min(W + (Padding * 2), MaxW) PaddedH = min(H + (Padding * 2), MaxH) BoundingBoxes.append((PaddedX, PaddedY, PaddedW, PaddedH)) Logger.info(f'📊 Filtered {ValidContours} contours based on MinArea ({MinArea}).') InitialBoxCount = len(BoundingBoxes) MergedBoundingBoxes = MergeBoxes(BoundingBoxes, MergePadding) EndTime = time.time() if MergedBoundingBoxes: Logger.info(f'📦 Merged {InitialBoxCount} boxes into {len(MergedBoundingBoxes)} regions.') else: Logger.info('❌ No significant changed regions found after filtering and merging.') Logger.info(f'⏱️ Region finding took {EndTime - StartTime:.3f}s') return MergedBoundingBoxes # Example call for the new visualization function VisualizeDifferences(r'C:\Users\joris\Pictures\frame_01660.png', r'C:\Users\joris\Pictures\frame_01661.png', './Diff.png', 25, 100, (0, 255, 0), (0, 180, 0), 0.3)