File size: 6,897 Bytes
b09e573
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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)