From 9048c73ce7dba20092fa829be80be233eef61674 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Wed, 5 Aug 2020 14:42:31 -0700 Subject: [PATCH 1/9] WIP: Implement a new way to subtract baseline pigmentation From a csv file we get by running the program on the WT, we can pass a path to that csv file and use it to subtract the values --- .../Additions.swift | 74 ++++++++++++++++++- .../ColonyPigmentationAnalysis/Tasks.swift | 2 + Sources/ColonyPigmentationAnalysis/main.swift | 51 +++++++++++-- .../ColonyPigmentation.swift | 24 +++++- group-run.sh | 10 +++ 5 files changed, 146 insertions(+), 15 deletions(-) create mode 100755 group-run.sh diff --git a/Sources/ColonyPigmentationAnalysis/Additions.swift b/Sources/ColonyPigmentationAnalysis/Additions.swift index e32c2c4..e0ffc9f 100644 --- a/Sources/ColonyPigmentationAnalysis/Additions.swift +++ b/Sources/ColonyPigmentationAnalysis/Additions.swift @@ -50,14 +50,22 @@ extension CSV: StorableInDisk { } } +extension Array where Element == PigmentationSample { + fileprivate static let csvHeader = "x, average, stddev, columns" + + var csv: CSV { + let header = "\(Self.csvHeader)\n" + let contents = map({ "\($0.x),\($0.averagePigmentation),\($0.standardDeviation),\($0.includedColumnIndices.map(String.init).joined(separator: "-"))" }).joined(separator: "\n") + + return CSV(contents: header + contents) + } +} + extension Array: StorableInDisk where Element == PigmentationSample { static var fileExtension: String { "csv" } func save(toPath path: String) throws { - let header = "x, average, stddev, columns\n" - let contents = map({ "\($0.x),\($0.averagePigmentation),\($0.standardDeviation),\($0.includedColumnIndices.map(String.init).joined(separator: "-"))" }).joined(separator: "\n") - - try CSV(contents: header + contents).save(toPath: path) + try csv.save(toPath: path) } } @@ -68,3 +76,61 @@ func createDirectory(_ directory: String) throws { throw TaskError.failedToCreateResultDirectory(path: directory, underlyingError: error) } } + +func readPigmentationHistogram(at path: String) throws -> [PigmentationSample] { + enum InvalidCSVFormatError: CustomNSError, LocalizedError { + case invalidHeader(contents: String) + case invalidNumberOfRows(Int) + case invalidRowFormat(rowIndex: Int, contents: String) + case invalidValueFormat(expectedType: Any.Type, value: String) + + static let errorDomain = "InvalidCSVFormatError" + + var errorDescription: String? { + switch self { + case let .invalidHeader(contents): return "Invalid header: \(contents). Expected \"\([PigmentationSample].csvHeader)\"" + case let .invalidNumberOfRows(rows): return "Invalid number of rows: \(rows). Expected > 1" + case let .invalidRowFormat(rowIndex, contents): return "Invalid row format at \(rowIndex): \(contents)" + case let .invalidValueFormat(expectedType, value): return "Invalid value format: \(value). Expected \(expectedType)" + } + } + } + + let contents = try String(contentsOfFile: path) + let rows = contents.split(separator: "\n") + guard rows.count > 1 else { + throw InvalidCSVFormatError.invalidNumberOfRows(rows.count) + } + + guard rows[0] == [PigmentationSample].csvHeader else { + throw InvalidCSVFormatError.invalidHeader(contents: String(rows[0])) + } + + let rowsWithoutHeader = rows.dropFirst() + + return try rowsWithoutHeader.enumerated().map { (index, row) in + let columns = row.split(separator: ",") + guard columns.count == 4 else { + throw InvalidCSVFormatError.invalidRowFormat(rowIndex: index, contents: String(row)) + } + + guard let x = Double(columns[0]) else { + throw InvalidCSVFormatError.invalidValueFormat(expectedType: Double.self, value: String(columns[0])) + } + guard let averagePigmentation = Double(columns[1]) else { + throw InvalidCSVFormatError.invalidValueFormat(expectedType: Double.self, value: String(columns[1])) + } + + guard let standardDeviation = Double(columns[2]) else { + throw InvalidCSVFormatError.invalidValueFormat(expectedType: Double.self, value: String(columns[2])) + } + let includedColumnIndices = try columns[3].split(separator: "-").map { (value: Substring) throws -> Int in + guard let intValue = Int(value) else { + throw InvalidCSVFormatError.invalidValueFormat(expectedType: Int.self, value: String(value)) + } + return intValue + } + + return PigmentationSample(x: x, averagePigmentation: averagePigmentation, standardDeviation: standardDeviation, includedColumnIndices: includedColumnIndices) + } +} diff --git a/Sources/ColonyPigmentationAnalysis/Tasks.swift b/Sources/ColonyPigmentationAnalysis/Tasks.swift index 03a0913..7cd4398 100644 --- a/Sources/ColonyPigmentationAnalysis/Tasks.swift +++ b/Sources/ColonyPigmentationAnalysis/Tasks.swift @@ -95,6 +95,7 @@ let removeBackgroundTask = Task<(ImageMap, MaskBitMap), (), ImageMap>(name: "Rem struct PigmentationHistogramTaskConfiguration { let pigmentationColor: ColonyPigmentationAnalysisKit.RGBColor let baselinePigmentation: Double + let pigmentationValuesToSubtract: [Double]? let pigmentationAreaOfInterestHeightPercentage: Double let horizontalSamples: Int? } @@ -104,6 +105,7 @@ let pigmentationHistogramTask = Task<(ImageMap, MaskBitMap), PigmentationHistogr withColonyMask: input.1, keyColor: configuration.pigmentationColor, baselinePigmentation: configuration.baselinePigmentation, + pigmentationValuesToSubtract: configuration.pigmentationValuesToSubtract, areaOfInterestHeightPercentage: configuration.pigmentationAreaOfInterestHeightPercentage, horizontalSamples: configuration.horizontalSamples ) diff --git a/Sources/ColonyPigmentationAnalysis/main.swift b/Sources/ColonyPigmentationAnalysis/main.swift index bb5b28f..08dc8ab 100644 --- a/Sources/ColonyPigmentationAnalysis/main.swift +++ b/Sources/ColonyPigmentationAnalysis/main.swift @@ -44,6 +44,9 @@ struct Main: ParsableCommand { @Option(default: 0.436, help: "A minimum level of pigmentation that is considered 'background noise' and is subtracted from all values") var baselinePigmentation: Double + + @Option(default: nil, help: "A file to read a pigmentation histogram for to use as baseline values. If specified, this takes precendence over --baseline-pigmentation. It must be a file with the same format as a csv output by this program") + var baselinePigmentationHistogramFilePath: String? @Option(default: 200, help: "Output the pigmentation histogram csv by interpolating to this many values") var pigmentationHistogramSampleCount: Int @@ -56,13 +59,18 @@ struct Main: ParsableCommand { func run() throws { guard !images.isEmpty else { - Self.exit(withError: ValidationError("No images specified")) + throw ValidationError("No images specified") } logger.info("\("Analyzing \(String(images.count).onBlack()) images".blue())") try saveCurrentConfigurationToFile() + let pigmentationValuesToSubtract = try baselinePigmentationSamples()?.map { $0.averagePigmentation } + if let pigmentationValuesToSubtract = pigmentationValuesToSubtract { + precondition(pigmentationValuesToSubtract.count == pigmentationHistogramSampleCount, "The number of pigmentation samples to subtract provided via --baseline-pigmentation-histogram-file-path needs to match --pigmentation-histogram-sample-count") + } + let queue = DispatchQueue(label: "colony-analysis-queue", qos: .userInitiated, attributes: parallelize ? .concurrent : [], autoreleaseFrequency: .inherit, target: nil) var lastCaughtError: Swift.Error? @@ -70,7 +78,7 @@ struct Main: ParsableCommand { for (index, imagePath) in images.enumerated() { queue.async { do { - try self.analyzeImage(atPath: imagePath) + try self.analyzeImage(atPath: imagePath, withPigmentationValuesToSubtract: pigmentationValuesToSubtract) } catch { lastCaughtError = error logger.error("Error analyzing image \(index) (\(imagePath)): \(error)") @@ -85,6 +93,9 @@ struct Main: ParsableCommand { try averagedPigmentationSamples.save(toPath: self.outputPath.appending("average_pigmentation.\([PigmentationSample].fileExtension)")) try pigmentationSeriesTask.run(withInput: averagedPigmentationSamples, configuration: ()).save(toPath: self.outputPath.appending("average_pigmentation_1d.csv")) + +// let minPigmentation = PigmentationSample.minAveragePigmentation(Self.pigmentationSamplesWithoutBaseline) +// try CSV(contents: "\(minPigmentation)").save(toPath: self.outputPath.appending("min_pigmentation.txt")) } } catch { lastCaughtError = error @@ -103,8 +114,9 @@ Main.main() private extension Main { static var pigmentationSamples: [[PigmentationSample]] = [] + static var pigmentationSamplesWithoutBaseline: [[PigmentationSample]] = [] - func analyzeImage(atPath imagePath: String) throws { + func analyzeImage(atPath imagePath: String, withPigmentationValuesToSubtract pigmentationValuesToSubtract: [Double]?) throws { let imageName = ((imagePath as NSString).lastPathComponent as NSString).deletingPathExtension try measure(name: imageName) { @@ -141,19 +153,27 @@ private extension Main { try taskRunner.run( pigmentationHistogramTask, withInput: (image, colonyMask), - configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, pigmentationAreaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, horizontalSamples: nil), + configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, pigmentationValuesToSubtract: nil, pigmentationAreaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, horizontalSamples: nil), artifactDirectory: "RawPigmentationHistogram" ) let sampledPigmentation = try taskRunner.run( pigmentationHistogramTask, withInput: (image, colonyMask), - configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, pigmentationAreaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, horizontalSamples: pigmentationHistogramSampleCount), + configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, pigmentationValuesToSubtract: pigmentationValuesToSubtract, pigmentationAreaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, horizontalSamples: pigmentationHistogramSampleCount), artifactDirectory: "SampledPigmentationHistogram" ) +// let sampledPigmentationWithoutBaseline = try taskRunner.run( +// pigmentationHistogramTask, +// withInput: (image, colonyMask), +// configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: 0, pigmentationValuesToSubtract: nil, pigmentationAreaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, horizontalSamples: pigmentationHistogramSampleCount), +// artifactDirectory: "NoBaselineSampledPigmentationHistogram" +// ) + DispatchQueue.main.sync { Self.pigmentationSamples.append(sampledPigmentation) +// Self.pigmentationSamplesWithoutBaseline.append(sampledPigmentationWithoutBaseline) } try taskRunner.run( @@ -164,6 +184,12 @@ private extension Main { ) } } + + private func baselinePigmentationSamples() throws -> [PigmentationSample]? { + return try baselinePigmentationHistogramFilePath.map { + return try readPigmentationHistogram(at: $0) + } + } } extension Main { @@ -176,6 +202,16 @@ extension Main { guard downscaleFactor > 0 && downscaleFactor <= 1 else { throw ValidationError("downscale-factor must be a value between 0 and 1. Got \(downscaleFactor) instead") } guard (0...1).contains(backgroundChromaKeyThreshold) else { throw ValidationError("background-chroma-key-threshold must be a value between 0 and 1. Got \(backgroundChromaKeyThreshold) instead") } + guard (0...1).contains(baselinePigmentation) else { + throw ValidationError("baseline-pigmentation must be a value between 0 and 1. Got \(baselinePigmentation) instead") + } + + if let baselinePigmentationHistogramFilePath = baselinePigmentationHistogramFilePath { + guard FileManager.default.fileExists(atPath: baselinePigmentationHistogramFilePath) else { + throw ValidationError("--baseline-pigmentation-histogram-file-path \"\(baselinePigmentationHistogramFilePath)\" doesn't exist") + } + } + guard (0...1).contains(pigmentationAreaOfInterestHeightPercentage) else { throw ValidationError("pigmentation-roi-height must be a value between 0 and 1. Got \(pigmentationAreaOfInterestHeightPercentage) instead") } } @@ -193,14 +229,15 @@ private extension Main { func saveCurrentConfigurationToFile() throws { let configuration = """ Date: \(Date()) - Images: \(images.count) Downscale Factor: \(downscaleFactor) Background Chroma Key Color: \(backgroundChromaKeyColor) Background Chroma Key Threshold: \(backgroundChromaKeyThreshold) Pigmentation Color: \(pigmentationColor) - Baseline Pigmentation: \(baselinePigmentation) Pigmentation Histogram Sample Count: \(pigmentationHistogramSampleCount) Pigmentation Area of Interest Height Percentage: \(pigmentationAreaOfInterestHeightPercentage) + Images (\(images.count)): \(images.joined(separator: ", ")) + Baseline Pigmentation: \(baselinePigmentation) + Baseline Pigmentation Histogram Contents:\n\(try baselinePigmentationSamples()?.csv.contents ?? "N/A") """ let path = outputPath.appending("parameters.txt") diff --git a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift index 85c5dcd..f0e25b4 100644 --- a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift +++ b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift @@ -54,6 +54,12 @@ public struct PigmentationSample { return averagePigmentations } + + // TODO + public static func minAveragePigmentation(_ pigmentationSamples: [[PigmentationSample]]) -> Double { + let pigmentationAverages = pigmentationSamples.map { $0.map(\.averagePigmentation).average } + return pigmentationAverages.min()! + } } public extension ImageMap { @@ -63,6 +69,7 @@ public extension ImageMap { /// - keyColor: the color to compare all pixels to (considered maximum pigmentation) /// - baselinePigmentation: a cut-off value between 0 and 1 that would cause all pigmentation values below this to be considered not pigmented. /// Values above it would then be interpolated between that value and 1. + /// - pigmentationValuesToSubtract: An optional array of `horizontalSamples` values to use to subtract the pigmentation in each column. /// - areaOfInterestHeightPercentage: A value between 0 and 1 to use to only consider pixels at a given x location that are inside a rectangle of height /// determined by the height of the mask times this value. This can be used to only consider pigmentation across a /// central "band" in the colony, instead of every pixel. @@ -70,10 +77,15 @@ public extension ImageMap { /// (Must be less than the horizontal number of pixel columns in the colony) /// - Returns: An array of `PigmentationSample`s. func calculate2DPigmentationAverages(withColonyMask colonyMask: MaskBitMap, keyColor: RGBColor, - baselinePigmentation: Double = 0, areaOfInterestHeightPercentage: Double, - horizontalSamples: Int?) -> [PigmentationSample] { + baselinePigmentation: Double = 0, pigmentationValuesToSubtract: [Double]?, + areaOfInterestHeightPercentage: Double, horizontalSamples: Int?) + -> [PigmentationSample] { precondition(size == colonyMask.size, "The colony image and its mask should be the same size \(size) vs \(colonyMask.size)") precondition(areaOfInterestHeightPercentage.isNormalized, "areaOfInterestHeightPercentage should be a value between 0 and 1, got \(areaOfInterestHeightPercentage)") + if let pigmentationValuesToSubtract = pigmentationValuesToSubtract { + precondition(horizontalSamples != nil, "If pigmentation values to subtract are specified, then we need a fixed set of horizontal samples") + precondition(pigmentationValuesToSubtract.count == horizontalSamples, "The specified pigmentation values to subtract (\(pigmentationValuesToSubtract.count) values) does not match the number of horizontal samples (\(horizontalSamples ?? 0))") + } let areaOfInterest = colonyMask.areaOfInterestToCalculatePigmentationOfColonyImage(withHeightPercentage: areaOfInterestHeightPercentage) @@ -113,8 +125,12 @@ public extension ImageMap { if sampledColumnPixels.isEmpty { logger.warning("Found no white pixels in mask for sample index \(sampleIndex) (columns \(columns) in roi \(areaOfInterest)). This may be an error.") } - - let pigmentationValues = sampledColumnPixels.map({ $0.pigmentation(withKeyColor: keyColor, baselinePigmentation: baselinePigmentation) }) + let pigmentationValues = sampledColumnPixels.map { + max(0, + $0.pigmentation(withKeyColor: keyColor, baselinePigmentation: baselinePigmentation) + - (pigmentationValuesToSubtract?[sampleIndex] ?? 0) + ) + } samples.append((average: pigmentationValues.average, stddev: pigmentationValues.standardDeviation, columnIndices: Array(columns))) } diff --git a/group-run.sh b/group-run.sh new file mode 100755 index 0000000..4bc8ead --- /dev/null +++ b/group-run.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +ALL_GROUPS="delta6944 deltaA8 deltaB8 deltaC8 deltaD8 deltaGAF WT" + +for group in $ALL_GROUPS; do + output_dir="grouped-output/$group/" + mkdir -p "$output_dir" + echo "Running $group..." + ./run --output-path "$output_dir" --images "images/$group-*" --baseline-pigmentation 0.3426845566887504 --baseline-pigmentation-histogram-file-path ../baseline-wt-pigmentation.csv +done From cc1ee34a38fdde810dca22c5f35b3be0822b8f15 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Wed, 12 Aug 2020 15:40:07 -0700 Subject: [PATCH 2/9] Ignore grouped_output directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ee2da8f..448305c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /Packages xcuserdata/ /output/ +/grouped-output/ ColonyPigmentationAnalysis.xcodeproj From eb5c99b37b5adf7a6839da812d665305865f5127 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:13:13 -0700 Subject: [PATCH 3/9] Color difference tests --- .../ColorDistanceTests.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Tests/ColonyPigmentationAnalysisKitTests/ColorDistanceTests.swift diff --git a/Tests/ColonyPigmentationAnalysisKitTests/ColorDistanceTests.swift b/Tests/ColonyPigmentationAnalysisKitTests/ColorDistanceTests.swift new file mode 100644 index 0000000..53ad8c9 --- /dev/null +++ b/Tests/ColonyPigmentationAnalysisKitTests/ColorDistanceTests.swift @@ -0,0 +1,67 @@ +// +// ColorDistanceTests.swift +// ColonyPigmentationAnalysisKitTests +// +// Created by Javier Soto on 8/22/20. +// + +import Foundation +import XCTest +@testable import ColonyPigmentationAnalysisKit + +final class ColorDistanceTests: XCTestCase { + func testLABColorDistance() { + func distance(_ color1: ColonyPigmentationAnalysisKit.RGBColor, _ color2: ColonyPigmentationAnalysisKit.RGBColor) -> Double { + return LABColor(XYZColor(color1)).distance(to: LABColor(XYZColor(color2))) + } + + XCTAssertEqual(distance(.black, .black), 0) + XCTAssertEqual(distance(.white, .white), 0) + XCTAssertEqual(distance(.red, .red), 0) + XCTAssertEqual(distance(.green, .green), 0) + XCTAssertEqual(distance(.blue, .blue), 0) + XCTAssertEqual(distance(.white, .black), 1, accuracy: 0.001) + XCTAssertEqual(distance(.black, .white), 1, accuracy: 0.001) + XCTAssertEqual(distance(.black, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.white, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.white, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.red, .green), 0.9, accuracy: 0.1) + XCTAssertEqual(distance(.green, .blue), 1.3, accuracy: 0.1) + XCTAssertEqual(distance(.red, .blue), 0.86, accuracy: 0.1) + + XCTAssertEqual(distance(.testColor1, .testColor2), 0.22, accuracy: 0.1) + } + + func testLABColorNormalizedDistance() { + func distance(_ color1: ColonyPigmentationAnalysisKit.RGBColor, _ color2: ColonyPigmentationAnalysisKit.RGBColor) -> Double { + return LABColor(XYZColor(color1)).normalizedDistance(to: LABColor(XYZColor(color2))) + } + + XCTAssertEqual(distance(.black, .black), 0) + XCTAssertEqual(distance(.white, .white), 0) + XCTAssertEqual(distance(.red, .red), 0) + XCTAssertEqual(distance(.green, .green), 0) + XCTAssertEqual(distance(.blue, .blue), 0) + XCTAssertEqual(distance(.white, .black), 1, accuracy: 0.001) + XCTAssertEqual(distance(.black, .white), 1, accuracy: 0.001) + XCTAssertEqual(distance(.black, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.white, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.white, .red), 0.75, accuracy: 0.1) + XCTAssertEqual(distance(.red, .green), 0.9, accuracy: 0.1) + XCTAssertEqual(distance(.green, .blue), 1, accuracy: 0.1) + XCTAssertEqual(distance(.red, .blue), 0.86, accuracy: 0.1) + + XCTAssertEqual(distance(.testColor1, .testColor2), 0.35, accuracy: 0.1) + } +} + +private extension ColonyPigmentationAnalysisKit.RGBColor { + static let red = RGBColor(r: 255, g: 0, b: 0) + static let green = RGBColor(r: 0, g: 255, b: 0) + static let blue = RGBColor(r: 0, g: 0, b: 255) + + // pigmentation + static let testColor1 = RGBColor(rgbHexString: "803D33")! + // unpigmented colony color + static let testColor2 = RGBColor(rgbHexString: "767E7E")! +} From 39f9dfbf116b13613648f13a9c219563e772bd41 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:13:24 -0700 Subject: [PATCH 4/9] Remove unused method --- Sources/ColonyPigmentationAnalysisKit/Colors.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Sources/ColonyPigmentationAnalysisKit/Colors.swift b/Sources/ColonyPigmentationAnalysisKit/Colors.swift index 83b1dd2..effcf38 100644 --- a/Sources/ColonyPigmentationAnalysisKit/Colors.swift +++ b/Sources/ColonyPigmentationAnalysisKit/Colors.swift @@ -155,16 +155,6 @@ public struct XYZColor: Hashable { y = (redCoefficient * 0.2126) + (greenCoefficient * 0.7152) + (blueCoefficient * 0.0722) z = (redCoefficient * 0.0193) + (greenCoefficient * 0.1192) + (blueCoefficient * 0.9505) } - - public func distance(to color: XYZColor) -> Double { - let distance = sqrt( - pow(color.x - x, 2) - + pow(color.y - y, 2) - + pow(color.z - z, 2) - ) - - return distance > 0.4 ? 1 : distance - } } // MARK: - LAB Color Space From 901bee3381ff33604ee36da003d3e82593f83189 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:13:31 -0700 Subject: [PATCH 5/9] Remove assertions --- Sources/ColonyPigmentationAnalysisKit/Colors.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/ColonyPigmentationAnalysisKit/Colors.swift b/Sources/ColonyPigmentationAnalysisKit/Colors.swift index effcf38..6d89c85 100644 --- a/Sources/ColonyPigmentationAnalysisKit/Colors.swift +++ b/Sources/ColonyPigmentationAnalysisKit/Colors.swift @@ -227,8 +227,6 @@ public struct LABColor: Hashable { + pow(colorNormalizedComponents.b - selfNormalizedComponents.b, 2) ) - ColonyPigmentationAnalysisKit.assert(distance.isNormalized) - return distance } @@ -243,7 +241,6 @@ public struct LABColor: Hashable { let distanceToCenterOfSphere = distance(to: colorAtCenterOfSphere, ignoringLightness: ignoringLightness) let sphereRadius: Double = 0.5 let maximumDistance = distanceToCenterOfSphere + sphereRadius - ColonyPigmentationAnalysisKit.assert((0...1.1).contains(maximumDistance)) let distance = self.distance(to: color, ignoringLightness: ignoringLightness) From 3a45a50b97a540beeecc0fed4c9a6b3f32f843c7 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:13:44 -0700 Subject: [PATCH 6/9] Change repetition factor --- Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift index f0e25b4..3e7aac2 100644 --- a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift +++ b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift @@ -153,7 +153,7 @@ public extension Array where Element == PigmentationSample { /// Returns the x coordinates (from 0 to 1) of each column in `self`, repeated a number of times proportional to the pigmentation. func oneDimensionHistogram() -> [Double] { return flatMap { (pigmentationSample) -> [Double] in - let numberOfTimes = Int(pigmentationSample.averagePigmentation / 0.1) + let numberOfTimes = Int(pigmentationSample.averagePigmentation / 0.01) return [Double](repeating: pigmentationSample.x, count: numberOfTimes) } From b6323a50b6e0608fe35ba42d661e95922a0f8e07 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:14:17 -0700 Subject: [PATCH 7/9] Take pigmentation values to subtract into account for grayscale --- .../ColonyPigmentationAnalysis/Tasks.swift | 10 ++++--- Sources/ColonyPigmentationAnalysis/main.swift | 3 ++- .../ColonyPigmentation.swift | 27 +++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Sources/ColonyPigmentationAnalysis/Tasks.swift b/Sources/ColonyPigmentationAnalysis/Tasks.swift index 7cd4398..22f6d9c 100644 --- a/Sources/ColonyPigmentationAnalysis/Tasks.swift +++ b/Sources/ColonyPigmentationAnalysis/Tasks.swift @@ -122,14 +122,18 @@ let pigmentationSeriesTask = Task<[PigmentationSample], Void, CSV>(name: "Pigmen // MARK: - Draw Pigmentation struct DrawPigmentationTaskConfiguration { - let pigmentationColor: ColonyPigmentationAnalysisKit.RGBColor - let baselinePigmentation: Double + var pigmentationColor: ColonyPigmentationAnalysisKit.RGBColor + var baselinePigmentation: Double + var pigmentationValuesToSubtract: [Double]? = nil + var areaOfInterestHeightPercentage: Double = 1 } let drawPigmentationTask = Task<(ImageMap, MaskBitMap), DrawPigmentationTaskConfiguration, ImageMap>(name: "Draw Pigmentation") { input, configuration in return input.0.replacingColonyPixels( withMask: input.1, withPigmentationBasedOnKeyColor: configuration.pigmentationColor, - baselinePigmentation: configuration.baselinePigmentation + baselinePigmentation: configuration.baselinePigmentation, + pigmentationValuesToSubtract: configuration.pigmentationValuesToSubtract, + areaOfInterestHeightPercentage: configuration.areaOfInterestHeightPercentage ) } diff --git a/Sources/ColonyPigmentationAnalysis/main.swift b/Sources/ColonyPigmentationAnalysis/main.swift index 08dc8ab..671c0ab 100644 --- a/Sources/ColonyPigmentationAnalysis/main.swift +++ b/Sources/ColonyPigmentationAnalysis/main.swift @@ -146,7 +146,8 @@ private extension Main { try taskRunner.run( drawPigmentationTask, withInput: (image, colonyMask), - configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation), + configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, + pigmentationValuesToSubtract: pigmentationValuesToSubtract), artifactDirectory: "DrawnPigmentation" ) diff --git a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift index 3e7aac2..089ba18 100644 --- a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift +++ b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift @@ -194,15 +194,38 @@ private extension RGBColor { } public extension ImageMap { - func replacingColonyPixels(withMask mask: MaskBitMap, withPigmentationBasedOnKeyColor keyColor: RGBColor, baselinePigmentation: Double = 0) -> ImageMap { + func replacingColonyPixels(withMask mask: MaskBitMap, withPigmentationBasedOnKeyColor keyColor: RGBColor, + baselinePigmentation: Double = 0, pigmentationValuesToSubtract: [Double]?, + areaOfInterestHeightPercentage: Double) -> ImageMap { ColonyPigmentationAnalysisKit.assert(size == mask.size, "Image size \(size) must match mask size \(mask.size)") + precondition(areaOfInterestHeightPercentage.isNormalized, "areaOfInterestHeightPercentage should be a value between 0 and 1, got \(areaOfInterestHeightPercentage)") var copy = self + let maskBoundingRect = mask.areaOfInterestToCalculatePigmentationOfColonyImage(withHeightPercentage: areaOfInterestHeightPercentage) + copy.unsafeModifyPixels { (pixelIndex, pixel, pointer) in + let coordinate = rect.coordinate(forIndex: pixelIndex) + + guard maskBoundingRect.contains(coordinate) else { + pixel = .black + return + } + switch mask.pixels[pixelIndex] { case .white: - let pigmentation = pixel.pigmentation(withKeyColor: keyColor, baselinePigmentation: baselinePigmentation) + var pigmentation = pixel.pigmentation(withKeyColor: keyColor, baselinePigmentation: baselinePigmentation) + if let pigmentationValuesToSubtract = pigmentationValuesToSubtract, + !pigmentationValuesToSubtract.isEmpty { + let columnIndexInAreaOfInterest = Double(coordinate.x - maskBoundingRect.origin.x) + let arrayRange = (0...Double(pigmentationValuesToSubtract.count - 1)) + + let breakpointIndex = Int((0...(Double(maskBoundingRect.size.width - 1))) + .projecting(columnIndexInAreaOfInterest, into: arrayRange)) + + let baselineValueToSubtract = pigmentationValuesToSubtract[breakpointIndex] + pigmentation -= baselineValueToSubtract + } let gray = UInt8(max(0, min(1, pigmentation)) * 255) pixel = RGBColor(r: gray, g: gray, b: gray) From d9d858d6e44ca28428130ac6c92913b5587530d9 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Sat, 29 Aug 2020 17:14:28 -0700 Subject: [PATCH 8/9] Output grayscale images with just ROI too --- Sources/ColonyPigmentationAnalysis/main.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/ColonyPigmentationAnalysis/main.swift b/Sources/ColonyPigmentationAnalysis/main.swift index 671c0ab..47fde27 100644 --- a/Sources/ColonyPigmentationAnalysis/main.swift +++ b/Sources/ColonyPigmentationAnalysis/main.swift @@ -151,6 +151,15 @@ private extension Main { artifactDirectory: "DrawnPigmentation" ) + try taskRunner.run( + drawPigmentationTask, + withInput: (image, colonyMask), + configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, + pigmentationValuesToSubtract: pigmentationValuesToSubtract, + areaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage), + artifactDirectory: "DrawnPigmentationROI" + ) + try taskRunner.run( pigmentationHistogramTask, withInput: (image, colonyMask), From 7c024567e206d8c35089881c66d39776e8b3fef1 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Wed, 2 Sep 2020 11:24:03 -0700 Subject: [PATCH 9/9] Crop images where we only show the ROI --- .../ColonyPigmentationAnalysis/Tasks.swift | 19 +++++++++++++------ Sources/ColonyPigmentationAnalysis/main.swift | 6 +++--- .../ColonyPigmentation.swift | 8 ++++++++ .../ImageMap+Drawing.swift | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Sources/ColonyPigmentationAnalysis/Tasks.swift b/Sources/ColonyPigmentationAnalysis/Tasks.swift index 22f6d9c..6917bcb 100644 --- a/Sources/ColonyPigmentationAnalysis/Tasks.swift +++ b/Sources/ColonyPigmentationAnalysis/Tasks.swift @@ -126,14 +126,21 @@ struct DrawPigmentationTaskConfiguration { var baselinePigmentation: Double var pigmentationValuesToSubtract: [Double]? = nil var areaOfInterestHeightPercentage: Double = 1 + var cropWithinAreaOfInterest: Bool = false } let drawPigmentationTask = Task<(ImageMap, MaskBitMap), DrawPigmentationTaskConfiguration, ImageMap>(name: "Draw Pigmentation") { input, configuration in - return input.0.replacingColonyPixels( - withMask: input.1, - withPigmentationBasedOnKeyColor: configuration.pigmentationColor, - baselinePigmentation: configuration.baselinePigmentation, - pigmentationValuesToSubtract: configuration.pigmentationValuesToSubtract, - areaOfInterestHeightPercentage: configuration.areaOfInterestHeightPercentage + var result = input.0.replacingColonyPixels( + withMask: input.1, + withPigmentationBasedOnKeyColor: configuration.pigmentationColor, + baselinePigmentation: configuration.baselinePigmentation, + pigmentationValuesToSubtract: configuration.pigmentationValuesToSubtract, + areaOfInterestHeightPercentage: configuration.areaOfInterestHeightPercentage ) + + if configuration.cropWithinAreaOfInterest { + result.removePixelsOutsideAreaOfInterest(withMask: input.1, areaOfInterestHeightPercentage: configuration.areaOfInterestHeightPercentage) + } + + return result } diff --git a/Sources/ColonyPigmentationAnalysis/main.swift b/Sources/ColonyPigmentationAnalysis/main.swift index 47fde27..ef92f53 100644 --- a/Sources/ColonyPigmentationAnalysis/main.swift +++ b/Sources/ColonyPigmentationAnalysis/main.swift @@ -146,8 +146,7 @@ private extension Main { try taskRunner.run( drawPigmentationTask, withInput: (image, colonyMask), - configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, - pigmentationValuesToSubtract: pigmentationValuesToSubtract), + configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation), artifactDirectory: "DrawnPigmentation" ) @@ -156,7 +155,8 @@ private extension Main { withInput: (image, colonyMask), configuration: .init(pigmentationColor: pigmentationColor, baselinePigmentation: baselinePigmentation, pigmentationValuesToSubtract: pigmentationValuesToSubtract, - areaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage), + areaOfInterestHeightPercentage: pigmentationAreaOfInterestHeightPercentage, + cropWithinAreaOfInterest: true), artifactDirectory: "DrawnPigmentationROI" ) diff --git a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift index 089ba18..bcfa0e2 100644 --- a/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift +++ b/Sources/ColonyPigmentationAnalysisKit/ColonyPigmentation.swift @@ -236,6 +236,14 @@ public extension ImageMap { return copy } + + mutating func removePixelsOutsideAreaOfInterest(withMask mask: MaskBitMap, areaOfInterestHeightPercentage: Double) { + precondition(areaOfInterestHeightPercentage.isNormalized, "areaOfInterestHeightPercentage should be a value between 0 and 1, got \(areaOfInterestHeightPercentage)") + + let areaOfInterest = mask.areaOfInterestToCalculatePigmentationOfColonyImage(withHeightPercentage: areaOfInterestHeightPercentage) + + crop(with: areaOfInterest) + } } private extension MaskBitMap { diff --git a/Sources/ColonyPigmentationAnalysisKit/ImageMap+Drawing.swift b/Sources/ColonyPigmentationAnalysisKit/ImageMap+Drawing.swift index 0439067..ecaecc2 100644 --- a/Sources/ColonyPigmentationAnalysisKit/ImageMap+Drawing.swift +++ b/Sources/ColonyPigmentationAnalysisKit/ImageMap+Drawing.swift @@ -18,6 +18,24 @@ extension ImageMap { } } } + + mutating func crop(with rect: Rect) { + precondition(self.rect.contains(rect), "The provided rect \(rect) is not contained within \(self.rect)") + + var image = ImageMap(size: rect.size, pixels: [RGBColor](repeating: .black, count: rect.size.area)) + + image.unsafeModifyPixels { (index, pixel, _) in + let roiCoordinate = rect.coordinate(forIndex: index) + let originalImageCoordinate = Coordinate( + x: roiCoordinate.x + rect.minX, + y: roiCoordinate.y + rect.minY + ) + + pixel = self[originalImageCoordinate] + } + + self = image + } } extension MaskBitMap {