예제 #1
0
def main():
    print("started")
    config = ConfigProvider.config()

    # read data
    inspected = cv2.imread(config.data.defective_inspected_path1, 0).astype('float32')
    reference = cv2.imread(config.data.defective_reference_path1, 0).astype('float32')

    # registration
    aligner = Aligner()
    warped, warp_mask = aligner.align_images(static=inspected, moving=reference)

    # find defects
    defect_segmenter = DefectSegmenter()
    defect_mask = defect_segmenter.segment_defects(inspected, warped, warp_mask)

    # observe results
    diff = np.zeros(inspected.shape, dtype=np.float32)
    diff[warp_mask] = (np.abs((np.float32(warped) - np.float32(inspected))))[warp_mask]
    noise_cleaner = NoiseCleaner()
    diff = noise_cleaner.clean_frame(diff, warp_mask)

    cv2.imshow("color_result", get_color_diff_image(inspected, defect_mask * 255).astype('uint8'))
    plt.imshow(diff.astype('uint8'), cmap='gray')
    plt.title("diff")
    cv2.imshow("inspected", inspected.astype('uint8'))
    cv2.imshow("reference", reference.astype('uint8'))
    cv2.imshow("result", defect_mask.astype('uint8') * 255)

    plt.show()
    cv2.waitKey(0)
예제 #2
0
class BlurredDiffSegmenter(BaseSegmenter):
    """
    Use lower threshold, but lose some accuracy due to bluring.
    """
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._blured_diff_thres = self._config.detection.blured_diff_thres

    @overrides
    def detect(self, inspected, warped, warp_mask):
        diff = np.zeros(inspected.shape, dtype=np.float32)
        diff[warp_mask] = (np.abs(
            (np.float32(warped) - np.float32(inspected))))[warp_mask]

        diff = self._noise_cleaner.clean_frame(diff, warp_mask)

        # make sure noise is not interfering
        diff_blured = self._noise_cleaner.blur(diff, sigma=7)
        detection_mask = self._blured_diff_thres < diff_blured

        # plots
        plot_image(diff, "diff")
        show_color_diff(warped, inspected, "color diff")
        plot_image(diff_blured, "diff_blured")
        plot_image(detection_mask, "diff_based_segmentation")

        return detection_mask
예제 #3
0
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._min_diff_threshold = self._config.refinement.min_diff_threshold
        self._dilation_diameter = self._config.refinement.dilation_diameter
        self._min_new_connected_component_size = self._config.refinement.min_new_connected_component_size
예제 #4
0
class DefectSegmentationRefiner(object):
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._min_diff_threshold = self._config.refinement.min_diff_threshold
        self._dilation_diameter = self._config.refinement.dilation_diameter
        self._min_new_connected_component_size = self._config.refinement.min_new_connected_component_size

    def refine_segmentation(self, focused_defect_mask, inspected, warped,
                            warp_mask):
        """
        see also Segmenter.infer_region_statistics() for more details.
        Following commented is the start of a solution that I didn't have time to complete, but is another idea:
        After a good segmentation, statistics of pixel color per segment type (there are 3) can be obtained.
        Then, each pixel can be classified as defect/good by its color relative to its segment.
        Early research showed there are 3 gaussian peaks, roughly centered at gray levels 50, 100, 150.

        I didn't have time to create a full working solution like this because my segmentation was lacking.

        max_value_per_pixel = np.zeros_like(diff)
        min_value_per_pixel = np.zeros_like(diff)
        for c in range(config.segmentation.num_classes):
            max_value_per_pixel[warped_segmented == c] = statistics_per_class[c][0] + 2 * statistics_per_class[c][1]
            min_value_per_pixel[warped_segmented == c] = statistics_per_class[c][0] - 2 * statistics_per_class[c][1]

        clean_defect_mask = np.zeros_like(dirty_defect_mask)
        clean_defect_mask[dirty_defect_mask] = max_value_per_pixel[dirty_defect_mask] < inspected[dirty_defect_mask]
        clean_defect_mask[dirty_defect_mask] = np.logical_or(clean_defect_mask[dirty_defect_mask], min_value_per_pixel[dirty_defect_mask] < inspected[dirty_defect_mask])
        plot_image(max_value_per_pixel, "max_value_per_pixel")
        plot_image(min_value_per_pixel, "min_value_per_pixel")
        """

        diff = np.zeros(inspected.shape, dtype=np.float32)
        diff[warp_mask] = (np.abs(
            (np.float32(warped) - np.float32(inspected))))[warp_mask]

        diff = self._noise_cleaner.clean_frame(diff, warp_mask)

        # enlarge detection area in case of close proximity misses
        dilated_defect_mask = \
            self._noise_cleaner.dilate(focused_defect_mask.copy().astype('uint8'), diameter=self._dilation_diameter).astype(np.bool)

        diff_above_thres_mask = self._min_diff_threshold < diff
        dilated_defect_mask_with_artifacts = np.logical_and(
            diff_above_thres_mask, dilated_defect_mask)

        # clean stray pixels which were added due to the dilation, and passed the threshold
        enlarged_defect_mask_too_clean = self._noise_cleaner.clean_stray_pixels_bw(
            dilated_defect_mask_with_artifacts,
            min_size=self._min_new_connected_component_size)

        # but not ones that were present before dilation
        clean_defect_mask = np.logical_or(enlarged_defect_mask_too_clean,
                                          focused_defect_mask)

        plot_image(inspected, "inspected")
        plot_image(focused_defect_mask, "focused_defect_mask")
        plot_image(clean_defect_mask, "clean_defect_mask")
        return clean_defect_mask
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._thread_defect_high_pass_thres = self._config.detection.thread_defect_high_pass_thres
        self._aura_radius = self._config.detection.aura_radius
        self._low_diff_far_from_edge_thres = self._config.detection.low_diff_far_from_edge_thres
        self._min_thread_defect_size = self._config.detection.min_thread_defect_size
    def __init__(self):
        self._config = ConfigProvider.config()
        self._is_force_translation = self._config.alignment.is_force_translation
        self._subpixel_accuracy_resolution = self._config.alignment.subpixel_accuracy_resolution

        self._detector = cv2.ORB_create()
        self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
        self._noise_cleaner = NoiseCleaner()
    def __init__(self):
        self._config = ConfigProvider.config()
        self._num_classes = self._config.segmentation.num_classes
        self._low_threshold = self._config.segmentation.low_threshold
        self._high_threshold = self._config.segmentation.high_threshold
        self._auto_thresholds = self._config.segmentation.auto_thresholds

        self._noise_cleaner = NoiseCleaner()
        self._kmeans = KMeans(n_clusters=self._num_classes)
예제 #8
0
def _observe_results(defect_mask, inspected, reference, warp_mask, warped):
    diff = np.zeros(inspected.shape, dtype=np.float32)
    diff[warp_mask] = (np.abs((np.float32(warped) - np.float32(inspected))))[warp_mask]
    noise_cleaner = NoiseCleaner()
    diff = noise_cleaner.clean_frame(diff, warp_mask)
    # cv2.imshow("color_result", get_color_diff_image(inspected, defect_mask * 255).astype('uint8'))
    # plt.imshow(diff.astype('uint8'), cmap='gray')
    # plt.title("diff")
    # cv2.imshow("inspected", inspected.astype('uint8'))
    # cv2.imshow("reference", reference.astype('uint8'))
    # cv2.imshow("result", defect_mask.astype('uint8') * 255)
    plt.show()
    cv2.waitKey(0)
class DiffSegmenter(BaseSegmenter):
    """
    simplest segmenter, by using a high threshold for noise
    """
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._diff_thres = self._config.detection.diff_thres

    @overrides
    def detect(self, inspected, warped, warp_mask):
        diff = np.zeros(inspected.shape, dtype=np.float32)
        diff[warp_mask] = (np.abs(
            (np.float32(warped) - np.float32(inspected))))[warp_mask]

        diff = self._noise_cleaner.clean_frame(diff, warp_mask)

        detection_mask = self._diff_thres < diff

        # plots
        plot_image(diff, "diff")
        show_color_diff(warped, inspected, "color diff")
        plot_image(detection_mask, "diff_based_segmentation")

        return detection_mask
class ThreadDefectSegmenter(BaseSegmenter):
    """
    find "thin" defects, which are not very different from the background, and not in existing edges proximity.
    """
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._thread_defect_high_pass_thres = self._config.detection.thread_defect_high_pass_thres
        self._aura_radius = self._config.detection.aura_radius
        self._low_diff_far_from_edge_thres = self._config.detection.low_diff_far_from_edge_thres
        self._min_thread_defect_size = self._config.detection.min_thread_defect_size

    @overrides
    def detect(self, inspected, warped, warp_mask):
        i_blured = self._noise_cleaner.blur(inspected, sigma=10)
        high_pass = np.abs(np.float32(i_blured) - np.float32(inspected))
        plot_image(i_blured, "i_blured")
        plot_image(inspected, "inspected")
        show_color_diff(i_blured, inspected, "high_pass")

        # find and dilate edges, to get rid of high diff in high pass image caused by real edges
        # this will leave us only with edges caused by stuff that weren't in the original image.
        # obvious limitation: won't find items near real edges in original image.
        edges = cv2.Canny(warped.astype('uint8'), 100, 200) > 0
        edges_dialated = self._noise_cleaner.dilate(edges.astype(np.float32),
                                                    self._aura_radius)
        # blur to avoid noise
        high_pass_no_real_edges = high_pass.copy()
        high_pass_no_real_edges[~warp_mask] = 0
        high_pass_no_real_edges[edges_dialated > 0] = 0
        plot_image(high_pass_no_real_edges, "high_pass_no_real_edges")

        thread_defect_mask_noisy = self._thread_defect_high_pass_thres < high_pass_no_real_edges
        # here we have some false positives, which are caused by noise.

        # this detector finds "thread-like" defects, so I require the defects to be connected and at some min size.
        thread_defect_mask_clean = self._noise_cleaner.clean_stray_pixels_bw(
            thread_defect_mask_noisy, min_size=self._min_thread_defect_size)

        thread_defect_mask_closure = self._noise_cleaner.close(
            thread_defect_mask_clean.astype('uint8'), diameter=3, iterations=1)

        plot_image(thread_defect_mask_noisy, "thread_defect_mask_noisy")
        plot_image(thread_defect_mask_clean, "thread_defect_mask_clean")
        plot_image(thread_defect_mask_closure, "thread_defect_mask_closure")
        return thread_defect_mask_clean
class LowDiffFarFromEdgeSegmenter(BaseSegmenter):
    """
    find defects using a very low diff threshold,
    but only in case they are far enough from edges in the reference (warped)
    """
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._aura_radius = self._config.detection.aura_radius
        self._low_diff_far_from_edge_thres = self._config.detection.low_diff_far_from_edge_thres

    @overrides
    def detect(self, inspected, warped, warp_mask):
        diff = np.zeros(inspected.shape, dtype=np.float32)
        diff[warp_mask] = (np.abs(
            (np.float32(warped) - np.float32(inspected))))[warp_mask]

        diff = self._noise_cleaner.clean_frame(diff, warp_mask)

        # find and dilate edges, to get rid of high diff caused by suboptimal registration
        # and is most apparent near real edges.
        edges = cv2.Canny(warped.astype('uint8'), 100, 200) > 0
        edges_dialated = self._noise_cleaner.dilate(edges.astype(np.float32),
                                                    self._aura_radius)

        # blur to avoid noise
        diff_blured = self._noise_cleaner.blur(diff.copy(), sigma=5)
        #disregard area of edges
        diff_no_edges_blured = diff_blured
        diff_no_edges_blured[edges_dialated > 0] = 0
        low_diff_far_from_edge_mask = self._low_diff_far_from_edge_thres < diff_no_edges_blured

        plot_image(edges, "edges")
        plot_image(edges_dialated, "edges_dilated")
        plot_image(diff_no_edges_blured, "diff_no_edges_blured")
        plot_image(low_diff_far_from_edge_mask, "low_diff_far_from_edge_mask")
        return low_diff_far_from_edge_mask
class Segmenter(object):
    """
    This class was another attempt at the problem, which I didn't have time to complete.
    Main entry point is infer_region_statistics(), see doc there.
    """
    def __init__(self):
        self._config = ConfigProvider.config()
        self._num_classes = self._config.segmentation.num_classes
        self._low_threshold = self._config.segmentation.low_threshold
        self._high_threshold = self._config.segmentation.high_threshold
        self._auto_thresholds = self._config.segmentation.auto_thresholds

        self._noise_cleaner = NoiseCleaner()
        self._kmeans = KMeans(n_clusters=self._num_classes)

    def _perform_thresholding(self, image):
        clean = image.copy()
        under_low_mask = clean < self._low_threshold
        above_high_mask = self._high_threshold < clean
        middle_mask = ~np.logical_or(under_low_mask, above_high_mask)
        clean[under_low_mask] = 0
        clean[middle_mask] = 1
        clean[above_high_mask] = 2

        return clean

    @staticmethod
    def _find_local_minima(a):
        mins_mask = np.r_[True, a[1:] < a[:-1]] & np.r_[a[:-1] < a[1:], True]
        mins_inds = np.where(mins_mask)[0]
        mins = a[mins_inds]
        return mins, mins_inds

    @staticmethod
    def _smooth_curve(curve, kernel_size=5):
        kernel = np.ones((kernel_size, )) / kernel_size
        smooth = np.convolve(curve, kernel, mode='same')
        return smooth

    def _infer_thresholds_by_histogram(self, image):
        """
        assuming 3 intensity levels, and a valid input image (non defective)
        :return: low and high thresholds by which the image can be roughly segmented.
        """
        n_bins = 256
        threshold_factor = (256 // n_bins)
        hist, bins = np.histogram(image.flatten(), bins=n_bins)

        smooth_hist = hist
        smooth_hist = self._smooth_curve(smooth_hist, 20 // threshold_factor)
        smooth_hist = self._smooth_curve(smooth_hist, 14 // threshold_factor)
        smooth_hist = self._smooth_curve(smooth_hist, 10 // threshold_factor)
        smooth_hist = self._smooth_curve(smooth_hist, 6 // threshold_factor)

        # plt.figure()
        # plt.plot(hist, color="blue")
        # plt.plot(smooth_hist, color="red")
        # plt.show()

        mins, mins_inds = self._find_local_minima(smooth_hist)
        sorted_inds = sorted(
            filter(
                lambda x: 20 / threshold_factor < x < n_bins - 20 /
                threshold_factor, mins_inds))

        # assert len(sorted_inds) == self._num_classes - 1

        return sorted_inds[0] * threshold_factor, sorted_inds[
            1] * threshold_factor, hist, smooth_hist

    def segment_image_by_kmeans(self, image):
        clean = image
        clean = self._noise_cleaner.equalize_histogram(clean)
        clean = self._noise_cleaner.clean_salt_and_pepper(clean)

        segmentation_map = np.reshape(
            self._kmeans.fit_predict(clean.reshape(-1, 1)),
            clean.shape).astype(np.uint8)

        return segmentation_map

    def segment_image_by_threshold(self, image):
        clean = image.copy()

        clean = self._noise_cleaner.equalize_histogram(clean)
        clean = self._noise_cleaner.clean_salt_and_pepper(clean)

        hist, smooth_hist = None, None
        if self._auto_thresholds:
            low_thres, high_thres, hist, smooth_hist = self._infer_thresholds_by_histogram(
                clean)
            # I allow the high thres to go down, and the low to go up, but not vice versa.
            # self._low_threshold, self._high_threshold = max(low_thres, self._low_threshold), min(high_thres, self._high_threshold)
            self._low_threshold, self._high_threshold = low_thres, high_thres

        segmentation_map = self._perform_thresholding(clean)
        return segmentation_map, hist, smooth_hist, self._low_threshold, self._high_threshold

    def infer_region_statistics(self, image, mask):
        """
        This was supposed to be used for determining 3 regions in the warped image, then calculate the color
        distribution within each segment, then know for each pixel its probability of being an outlier by color.
        Unfortunately, I didn't have the time to create a good enough segmentation per class and this was no good.
        """
        segment_image = self.segment_image_by_kmeans(image.astype('uint8'))
        segment_image[~mask] = self._config.segmentation.num_classes

        statistics_per_class = []
        for c in range(self._config.segmentation.num_classes):
            class_data = image[segment_image == c]
            m, s = class_data.mean(), class_data.std()
            statistics_per_class.append((m, s))

        plot_image(image, "image")
        plot_image(segment_image, "segment_image")
        return statistics_per_class, segment_image
class Aligner(object):
    def __init__(self):
        self._config = ConfigProvider.config()
        self._is_force_translation = self._config.alignment.is_force_translation
        self._subpixel_accuracy_resolution = self._config.alignment.subpixel_accuracy_resolution

        self._detector = cv2.ORB_create()
        self._matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
        self._noise_cleaner = NoiseCleaner()

    def align_using_feature_matching(self, static, moving):
        # this works, but since we know the shift is translation only, normxcorr is better
        kp1, des1 = self._detector.detectAndCompute(static, None)
        kp2, des2 = self._detector.detectAndCompute(moving, None)
        matches = self._matcher.match(des1, des2)

        assert len(matches) >= 4  # for perspective tform
        moving_pts = np.float32([kp1[m.queryIdx].pt
                                 for m in matches]).reshape(-1, 1, 2)
        static_pts = np.float32([kp2[m.trainIdx].pt
                                 for m in matches]).reshape(-1, 1, 2)
        tform, mask = cv2.findHomography(moving_pts, static_pts, cv2.RANSAC,
                                         5.0)
        tform = self._force_translation_only(tform)
        matches_mask = mask.ravel().tolist()

        matches_image = cv2.drawMatches(
            static,
            kp1,
            moving,
            kp2,
            matches,
            None,
            flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
            matchesMask=matches_mask,
            matchColor=(0, 255, 0),
        )

        itform = np.linalg.pinv(tform)
        return matches_image, itform

    def align_using_tform(self, static, moving, tform):
        warped = cv2.warpPerspective(moving, tform,
                                     (static.shape[1], static.shape[0]))
        warped_region_mask = self._find_warped_region(tform, moving, static)
        return warped, warped_region_mask

    def align_using_shift(self, static, moving, shift_xy):
        tform = np.eye(3)
        tform[0, 2] = shift_xy[1]
        tform[1, 2] = shift_xy[0]
        warped, warped_region_mask = self.align_using_tform(
            static, moving, tform)
        return warped, warped_region_mask

    @staticmethod
    def _find_warped_region(tform, moving, static):
        white = np.ones(moving.shape)
        warped = cv2.warpPerspective(white, tform,
                                     (static.shape[1], static.shape[0]))
        warped_region_mask = warped > 0
        return warped_region_mask

    def _force_translation_only(self, tform):
        if self._is_force_translation:
            tform[0, 1] = 0
            tform[1, 0] = 0
            tform[2, 0] = 0
            tform[2, 1] = 0

            tform[0, 0] = 1
            tform[1, 1] = 1
            print(f"forcing tform {tform} to translation only")

        return tform

    @staticmethod
    def normxcorr2(template, image, mode="full"):
        """
        credit: https://github.com/Sabrewarrior/normxcorr2-python/blob/master/normxcorr2.py
        Input arrays should be floating point numbers.
        :param template: N-D array, of template or filter you are using for cross-correlation.
        Must be less or equal dimensions to image.
        Length of each dimension must be less than length of image.
        :param image: N-D array
        :param mode: Options, "full", "valid", "same"
        full (Default): The output of fftconvolve is the full discrete linear convolution of the inputs.
        Output size will be image size + 1/2 template size in each dimension.
        valid: The output consists only of those elements that do not rely on the zero-padding.
        same: The output is the same size as image, centered with respect to the ‘full’ output.
        :return: N-D array of same dimensions as image. Size depends on mode parameter.
        """

        # If this happens, it is probably a mistake
        if np.ndim(template) > np.ndim(image) or \
                len([i for i in range(np.ndim(template)) if template.shape[i] > image.shape[i]]) > 0:
            print(
                "normxcorr2: TEMPLATE larger than IMG. Arguments may be swapped."
            )

        template = template - np.mean(template)
        image = image - np.mean(image)

        a1 = np.ones(template.shape)
        # Faster to flip up down and left right then use fftconvolve instead of scipy's correlate
        ar = np.flipud(np.fliplr(template))
        out = fftconvolve(image, ar.conj(), mode=mode)

        image = fftconvolve(np.square(image), a1, mode=mode) - \
                np.square(fftconvolve(image, a1, mode=mode)) / (np.prod(template.shape))

        # Remove small machine precision errors after subtraction
        image[np.where(image < 0)] = 0

        template = np.sum(np.square(template))
        out = out / np.sqrt(image * template)

        # Remove any divisions by 0 or very close to 0
        out[np.where(np.logical_not(np.isfinite(out)))] = 0

        return out

    def align_using_normxcorr(self, static, moving):
        """
        normxcorr is same as
        # conv_res = convolve2d(static, moving, mode='full')
        # best_location_conv = np.unravel_index(np.argmax(res), res.shape)
        but fast.
        """
        res = self.normxcorr2(static, moving, mode='full')
        best_location = np.unravel_index(np.argmax(res), res.shape)
        moving_should_be_strided_by = -(np.array(best_location) -
                                        np.array(moving.shape) + 1)

        return moving_should_be_strided_by

    @staticmethod
    def align_using_ecc(static, moving):
        number_of_iterations = 100
        termination_eps = 1e-10

        cc, tform = cv2.findTransformECC(
            static,
            moving,
            np.eye(3, dtype=np.float32)[:2, :],
            cv2.MOTION_TRANSLATION,
            criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT,
                      number_of_iterations, termination_eps),
            inputMask=np.ones(static.shape, dtype='uint8') * 255,
            gaussFiltSize=11)
        return tform

    def align_images(self, static, moving):
        # clean noise
        static_clean = self._noise_cleaner.clean_salt_and_pepper(static, 5)
        moving_clean = self._noise_cleaner.clean_salt_and_pepper(moving, 5)

        # enlarge to obtain subpixel accuracy
        static_enlarged = cv2.resize(static_clean, (0, 0),
                                     fx=self._subpixel_accuracy_resolution,
                                     fy=self._subpixel_accuracy_resolution)
        moving_enlarged = cv2.resize(moving_clean, (0, 0),
                                     fx=self._subpixel_accuracy_resolution,
                                     fy=self._subpixel_accuracy_resolution)

        # normxcorr alignment (translation only)
        moving_should_be_strided_by_factored = self.align_using_normxcorr(
            static=static_enlarged, moving=moving_enlarged)

        # return to normal size of translation
        moving_should_be_strided_by = np.array(
            moving_should_be_strided_by_factored
        ) / self._subpixel_accuracy_resolution

        # perform actual warp
        warped, warp_mask = self.align_using_shift(
            static.copy(), moving.copy(), moving_should_be_strided_by)

        # show result
        diff = np.zeros(static.shape, dtype=np.float32)
        diff[warp_mask] = (np.abs(
            (np.float32(warped) - np.float32(static))))[warp_mask]
        show_color_diff(warped, static, "registration")
        plot_image(diff, "diff")
        return warped, warp_mask
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._aura_radius = self._config.detection.aura_radius
        self._low_diff_far_from_edge_thres = self._config.detection.low_diff_far_from_edge_thres
예제 #15
0
    def __init__(self):
        self._config = ConfigProvider.config()
        self._noise_cleaner = NoiseCleaner()

        self._blured_diff_thres = self._config.detection.blured_diff_thres