-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathColonyMasking.swift
More file actions
153 lines (118 loc) · 6.33 KB
/
ColonyMasking.swift
File metadata and controls
153 lines (118 loc) · 6.33 KB
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
152
//
// ColonyMasking.swift
// ColonyPigmentationAnalysisKit
//
// Created by Javier Soto on 1/7/20.
// Copyright © 2020 Javier Soto. All rights reserved.
//
public extension ImageMap {
/// Removes the background from `self` by looking for pixels similar to `backgroundKeyColor`
/// Parameters:
/// - backgroundKeyColor: The color to compare the background pixels to.
/// - colorThreshold: A value between 0 and 1 that represents how sensitive the background removal is.
/// A higher value means the pixels must be more different from the background to be considered foreground.
/// A lower value means colors more different from `backgroundKeyColor` will still be considered background.
func maskColony(withBackgroundKeyColor backgroundKeyColor: RGBColor, colorThreshold: Double) -> MaskBitMap {
var result = removeBackground(withBackgroundKeyColor: backgroundKeyColor, colorThreshold: colorThreshold)
result.removeSmallShapeGroups()
return result
}
/// Removes the background from `self` using the provided `mask`.
func removingBackground(using mask: MaskBitMap) -> ImageMap {
ColonyPigmentationAnalysisKit.assert(size == mask.size, "Image size (\(size)) must match mask size (\(mask.size))")
var copy = self
copy.unsafeModifyPixels { (pixelIndex, pixel, _) in
if mask.pixels[pixelIndex] == .black {
pixel = .black
}
}
return copy
}
}
internal // @testable
extension ImageMap {
func removeBackground(withBackgroundKeyColor backgroundKeyColor: RGBColor, colorThreshold: Double) -> MaskBitMap {
precondition((0...1).contains(colorThreshold))
let backgroundLABColor = LABColor(XYZColor(backgroundKeyColor))
var maskBitMap = MaskBitMap(size: size, pixels: [MaskBitMap.Pixel].init(repeating: .black, count: size.area))
maskBitMap.unsafeModifyPixels { (pixelIndex, pixel, pointer) in
let colorDifference = LABColor(XYZColor(self.pixels[pixelIndex])).normalizedDistance(to: backgroundLABColor, ignoringLightness: false)
if colorDifference > colorThreshold {
pixel = .white
}
}
return maskBitMap
}
}
internal // @testable
extension MaskBitMap {
mutating func removeSmallShapeGroups() {
// Fill in gaps inside the colonies.
fillInShapesFromInside()
// Remove bubbles that may be big enough to not have been filtered out as noise
// and which now have been filled out.
removeGroupsOfAdjacentPixels(smallerThanPercentageOfArea: 0.02)
}
}
private extension MaskBitMap {
/// Remove bubbles that may have been big enough to not have been filtered out as noise
/// and which now have been filled out.
/// This method assumes that the areas have been filled out first by the masking code.
mutating func removeGroupsOfAdjacentPixels(smallerThanPercentageOfArea minPercentageOfArea: Double) {
flipColor(ofGroupsOfColor: .white, ifSmallerThanPercentageOfArea: minPercentageOfArea)
}
mutating func fillInShapesFromInside() {
flipColor(ofGroupsOfColor: .black, ifSmallerThanPercentageOfArea: 0.2)
}
mutating func flipColor(ofGroupsOfColor groupColor: MaskBitMap.Pixel, ifSmallerThanPercentageOfArea minPercentageOfArea: Double) {
var seenIndices: Set<Int> = []
seenIndices.reserveCapacity(pixels.count)
var stack: [Int] = []
stack.reserveCapacity(pixels.count)
var matchingColorPixelIndicesInGroup: Set<Int> = []
matchingColorPixelIndicesInGroup.reserveCapacity(pixels.count)
let imageRect = rect
unsafeModifyPixels { (index, pixel, pointer) in
guard pixel == groupColor && !seenIndices.contains(index) else { return }
stack = [index]
matchingColorPixelIndicesInGroup = []
while !stack.isEmpty {
let index = stack.popLast()!
matchingColorPixelIndicesInGroup.insert(index)
seenIndices.insert(index)
func append(_ coordinate: Coordinate) {
let index = imageRect.pixelIndex(for: coordinate)
if !seenIndices.contains(index) {
stack.append(index)
seenIndices.insert(index)
}
}
let coordinate = imageRect.coordinate(forIndex: index)
func appendNeighboringPixelIfWhite(offset: PixelSize) {
let neighborPixelCoordinate = coordinate + offset
guard imageRect.contains(neighborPixelCoordinate) else { return }
let neighborPixelIndex = imageRect.pixelIndex(for: neighborPixelCoordinate)
if pointer[neighborPixelIndex] == groupColor {
append(neighborPixelCoordinate)
}
}
// Look down first as those are gonna first the next pixels in the array
// and that's more cache-line-friendly
appendNeighboringPixelIfWhite(offset: PixelSize(width: 0, height: 1))
appendNeighboringPixelIfWhite(offset: PixelSize(width: 0, height: -1))
appendNeighboringPixelIfWhite(offset: PixelSize(width: -1, height: -1))
appendNeighboringPixelIfWhite(offset: PixelSize(width: -1, height: 0))
appendNeighboringPixelIfWhite(offset: PixelSize(width: -1, height: 1))
appendNeighboringPixelIfWhite(offset: PixelSize(width: 1, height: -1))
appendNeighboringPixelIfWhite(offset: PixelSize(width: 1, height: 0))
appendNeighboringPixelIfWhite(offset: PixelSize(width: 1, height: 1))
}
let minArea = Int(Double(imageRect.size.area) * minPercentageOfArea)
if matchingColorPixelIndicesInGroup.count < minArea {
for index in matchingColorPixelIndicesInGroup {
pointer[index] = groupColor.opposite
}
}
}
}
}