Skip to content

Commit

Permalink
Merge pull request #6 from Alex-Vasile/dng_adition
Browse files Browse the repository at this point in the history
Automatically Convert non-DNG raws to DNG
  • Loading branch information
martin-marek authored Nov 10, 2022
2 parents d48d4cd + fc6c7f1 commit 6fdfc02
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 122 deletions.
78 changes: 43 additions & 35 deletions burstphoto/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ struct MainView: View {
VStack{
Spacer()

Text("Drag & drop a burst of DNG images")
Text("Drag & drop a burst of raw image files")
.multilineTextAlignment(.center)
.font(.system(size: 20, weight: .medium))
.opacity(0.8)
Expand All @@ -92,7 +92,7 @@ struct MainView: View {

Spacer()

Text("*.dng, *.DNG")
Text("*.DNG, *.ARW, *.NEF…")
.font(.system(size: 14, weight: .light))
.italic()
.opacity(0.8)
Expand Down Expand Up @@ -154,17 +154,29 @@ struct ProcessingView: View {
let saving_as_num_of_images = 0

func progress_int_to_str(_ int: Int) -> String {
if progress.int < image_urls.count {
return "Loading \(image_urls[progress.int].lastPathComponent)..."
} else if progress.int < 2*image_urls.count {
return "Processing \(image_urls[progress.int - image_urls.count].lastPathComponent)..."
if progress.includes_conversion {
if progress.int < image_urls.count {
return "Converting images to DNG (this might take a while)..."
} else if progress.int < 2*image_urls.count {
return "Loading \(image_urls[progress.int % image_urls.count].lastPathComponent)..."
} else if progress.int < 3*image_urls.count {
return "Processing \(image_urls[progress.int % image_urls.count].lastPathComponent)..."
} else {
return "Saving processed image..."
}
} else {
return "Saving processed image..."
if progress.int < image_urls.count {
return "Loading \(image_urls[progress.int].lastPathComponent)..."
} else if progress.int < 2*image_urls.count {
return "Processing \(image_urls[progress.int % image_urls.count].lastPathComponent)..."
} else {
return "Saving processed image..."
}
}
}

var body: some View {
ProgressView(progress_int_to_str(progress.int), value: Double(progress.int), total: Double(2*image_urls.count + saving_as_num_of_images))
ProgressView(progress_int_to_str(progress.int), value: Double(progress.int), total: Double((progress.includes_conversion ? 3 : 2)*image_urls.count + saving_as_num_of_images))
.font(.system(size: 16, weight: .medium))
.opacity(0.8)
.padding(20)
Expand Down Expand Up @@ -211,8 +223,13 @@ struct SettingsView: View {
Text("High")
}
}.padding(20)

Spacer()

// TODO: A File picker

}
.frame(width: 350, height: 300)
.frame(width: 350)
.navigationTitle("Preferences")
}
}
Expand Down Expand Up @@ -269,13 +286,10 @@ struct MyDropDelegate: DropDelegate {
}

// if a directory was drag-and-dropped, convert it to a list of urls
all_file_urls = optionally_convert_dir_to_urls(all_file_urls)
image_urls = optionally_convert_dir_to_urls(all_file_urls)

// sort the urls alphabetically
all_file_urls.sort(by: {$0.path < $1.path})

// update the app's list of urls
image_urls = all_file_urls
image_urls.sort(by: {$0.path < $1.path})

// sync GUI
DispatchQueue.main.async {
Expand All @@ -286,32 +300,16 @@ struct MyDropDelegate: DropDelegate {
do {
// compute reference index (use the middle image)
let ref_idx = image_urls.count / 2

// align and merge the burst
let output_texture = try align_and_merge(image_urls: image_urls, progress: progress, ref_idx: ref_idx, search_distance: AppSettings.search_distance, tile_size: AppSettings.tile_size, robustness: AppSettings.robustness)

// set output image url
let in_url = image_urls[ref_idx]
let in_filename = in_url.deletingPathExtension().lastPathComponent
let out_filename = in_filename + "_merged"
let out_dir = NSHomeDirectory() + "/Pictures/Burst Photo/"
let out_path = out_dir + out_filename + ".dng"
out_url = URL(fileURLWithPath: out_path)

// create output directory
if !FileManager.default.fileExists(atPath: out_dir) {
try FileManager.default.createDirectory(atPath: out_dir, withIntermediateDirectories: true, attributes: nil)
}

// save the output image
try texture_to_dng(output_texture, in_url, out_url)
// align and merge the burst
out_url = try align_and_merge(image_urls: image_urls, progress: progress, ref_idx: ref_idx, search_distance: AppSettings.search_distance, tile_size: AppSettings.tile_size, robustness: AppSettings.robustness)

// inform the user about the saved image
app_state = .image_saved

} catch ImageIOError.load_error {
my_alert.title = "Unsupported format"
my_alert.message = "Image format not supported. Please use RAW DNG images only, converted directly from RAW files using Adobe Lightroom or Adobe DNG Converter. Avoid using processed (demosaiced) DNG images."
my_alert.message = "Image format not supported. Please only use unprocessed RAW or DNG images. Using RAW images requires Adobe DNG Converter to be installed on your Mac."
my_alert.show = true
DispatchQueue.main.async { app_state = .main }
} catch ImageIOError.save_error {
Expand All @@ -326,12 +324,22 @@ struct MyDropDelegate: DropDelegate {
DispatchQueue.main.async { app_state = .main }
} catch AlignmentError.inconsistent_extensions {
my_alert.title = "Inconsistent formats"
my_alert.message = "The dropped files heve inconsistent formats. Please make sure that all images are DNG files."
my_alert.message = "Please make sure that all images have the same format."
my_alert.show = true
DispatchQueue.main.async { app_state = .main }
} catch AlignmentError.inconsistent_resolutions {
my_alert.title = "Inconsistent resolution"
my_alert.message = "The dropped files heve inconsistent resolutions. Please make sure that all images are DNG files generated directly from camera RAW files using Adobe Lightroom or Adobe DNG Converter."
my_alert.message = "Please make sure that all images have the same resolution."
my_alert.show = true
DispatchQueue.main.async { app_state = .main }
} catch AlignmentError.missing_dng_converter {
my_alert.title = "Missing Adobe DNG Converter"
my_alert.message = "Only DNG files are supported natively. If you wish to use other RAW formats, please download and install Adobe DNG Converter. Burst Photo will then be able to process most RAW formats automatically."
my_alert.show = true
DispatchQueue.main.async { app_state = .main }
} catch AlignmentError.conversion_failed {
my_alert.title = "Conversion Failed"
my_alert.message = "Image format not supported. Please only use unprocessed RAW or DNG images."
my_alert.show = true
DispatchQueue.main.async { app_state = .main }
} catch {
Expand Down
123 changes: 57 additions & 66 deletions burstphoto/align.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ enum AlignmentError: Error {
case less_than_two_images
case inconsistent_extensions
case inconsistent_resolutions
case missing_dng_converter
case conversion_failed
}


Expand All @@ -25,6 +27,7 @@ struct TileInfo {
// class to store the progress of the align+merge
class ProcessingProgress: ObservableObject {
@Published var int = 0
@Published var includes_conversion = false
}


Expand Down Expand Up @@ -487,76 +490,60 @@ func robust_merge(_ ref_texture: MTLTexture, _ ref_texture_blurred: MTLTexture,
}


func load_images(_ urls: [URL], _ progress: ProcessingProgress) throws -> ([MTLTexture], Int) {
func align_and_merge(image_urls: [URL], progress: ProcessingProgress, ref_idx: Int = 0, search_distance: String = "Medium", tile_size: Int = 16, kernel_size: Int = 5, robustness: Double = 1) throws -> URL {

var textures_dict: [Int: MTLTexture] = [:]
let compute_group = DispatchGroup()
let compute_queue = DispatchQueue.global() // this is a concurrent queue to do compute
let access_queue = DispatchQueue(label: "") // this is a serial queue to read/save data thread-safely
var mosaic_pettern_width: Int?

for i in 0..<urls.count {
compute_queue.async(group: compute_group) {

// asynchronously load texture
if let (texture, _mosaic_pettern_width) = try? image_url_to_texture(urls[i], device) {

// sync GUI progress
DispatchQueue.main.async { progress.int += 1 }

// thread-safely save the texture
access_queue.sync {
textures_dict[i] = texture
mosaic_pettern_width = _mosaic_pettern_width
}
}
}
}

// wait until all the images are loaded
compute_group.wait()

// convert dict to list
var textures_list: [MTLTexture] = []
for i in 0..<urls.count {

// ensure thread-safety
try access_queue.sync {

// check whether the images have been loaded successfully
if let texture = textures_dict[i] {
textures_list.append(texture)
} else {
throw ImageIOError.load_error
}
}
// check that all images are of the same extension
let image_extension = image_urls[0].pathExtension
let all_extensions_same = image_urls.allSatisfy{$0.pathExtension == image_extension}
if !all_extensions_same {throw AlignmentError.inconsistent_extensions}

// check that 2+ images were provided
let n_images = image_urls.count
if n_images < 2 {throw AlignmentError.less_than_two_images}

// create output directory
let out_dir = NSHomeDirectory() + "/Pictures/Burst Photo/"
if !FileManager.default.fileExists(atPath: out_dir) {
try FileManager.default.createDirectory(atPath: out_dir, withIntermediateDirectories: true, attributes: nil)
}

return (textures_list, mosaic_pettern_width!)
}


func align_and_merge(image_urls: [URL], progress: ProcessingProgress, ref_idx: Int = 0, search_distance: String = "Medium", tile_size: Int = 16, kernel_size: Int = 5, robustness: Double = 1) throws -> MTLTexture {
// create a directory for temporary dngs inside the output directory
let tmp_dir = out_dir + ".dngs/"
try FileManager.default.createDirectory(atPath: tmp_dir, withIntermediateDirectories: true)

// check that 2+ images have been passed
if image_urls.count < 2 {
throw AlignmentError.less_than_two_images
}
// measure execution time
var t = DispatchTime.now().uptimeNanoseconds

// check that all images are of the same extension
let ref_ext = image_urls[0].pathExtension
for i in 1..<image_urls.count {
let comp_ext = image_urls[i].pathExtension
if comp_ext != ref_ext {
throw AlignmentError.inconsistent_extensions
// ensure that all files are .dng, converting them if necessary
var dng_urls = image_urls
let convert_to_dng = image_extension != "dng"
DispatchQueue.main.async { progress.includes_conversion = convert_to_dng }
if convert_to_dng {
// check if dng converter is installed
let dng_converter_path = "/Applications/Adobe DNG Converter.app"
if !FileManager.default.fileExists(atPath: dng_converter_path) {
// if dng coverter is not installed, prompt user
throw AlignmentError.missing_dng_converter
} else {
// the dng converter is installed -> use it
dng_urls = try convert_images_to_dng(image_urls, dng_converter_path, tmp_dir, progress)
print("Time to convert images: ", Float(DispatchTime.now().uptimeNanoseconds - t) / 1_000_000_000)
DispatchQueue.main.async { progress.int += n_images }
t = DispatchTime.now().uptimeNanoseconds
}
}

// set output location
let in_url = dng_urls[ref_idx]
let in_filename = in_url.deletingPathExtension().lastPathComponent
let out_filename = in_filename + "_merged"
let out_path = out_dir + out_filename + ".dng"
let out_url = URL(fileURLWithPath: out_path)

// load images
var t = DispatchTime.now().uptimeNanoseconds
var (textures, mosaic_pettern_width) = try load_images(image_urls, progress)
print("Time to load all images: ", Float(DispatchTime.now().uptimeNanoseconds - t) / 1_000_000_000)
let t0 = DispatchTime.now().uptimeNanoseconds
var (textures, mosaic_pettern_width) = try load_images(dng_urls, progress)
print("Time to load images: ", Float(DispatchTime.now().uptimeNanoseconds - t) / 1_000_000_000)
t = DispatchTime.now().uptimeNanoseconds

// convert images from uint16 to float16
textures = textures.map{texture_uint16_to_float($0)}
Expand Down Expand Up @@ -600,7 +587,7 @@ func align_and_merge(image_urls: [URL], progress: ProcessingProgress, ref_idx: I
fill_with_zeros(output_texture)

// iterate over comparison images
for comp_idx in 0..<image_urls.count {
for comp_idx in 0..<n_images {
// add the reference texture to the output
if comp_idx == ref_idx {
add_texture(ref_texture, output_texture)
Expand All @@ -625,7 +612,6 @@ func align_and_merge(image_urls: [URL], progress: ProcessingProgress, ref_idx: I

// align tiles
for i in (0 ... downscale_factor_array.count-1).reversed() {
t = DispatchTime.now().uptimeNanoseconds

// load layer params
let tile_size = tile_size_array[i]
Expand Down Expand Up @@ -676,9 +662,14 @@ func align_and_merge(image_urls: [URL], progress: ProcessingProgress, ref_idx: I
}

// rescale output texture
let output_texture_uint16 = average_texture_sums(output_texture, image_urls.count)
let output_texture_uint16 = average_texture_sums(output_texture, n_images)
print("Time to align+merge images: ", Float(DispatchTime.now().uptimeNanoseconds - t) / 1_000_000_000)

// save the output image
try texture_to_dng(output_texture_uint16, in_url, out_url)

print("Time to align+merge all images: ", Float(DispatchTime.now().uptimeNanoseconds - t0) / 1_000_000_000)
// delete the temporary dng directory
try FileManager.default.removeItem(atPath: tmp_dir)

return output_texture_uint16
return out_url
}
30 changes: 9 additions & 21 deletions burstphoto/cli.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ struct MyProgram {

// create a list of bursts to process
let burst_dirs = [
"/Users/martinmarek/My Drive/Coding/Burst/bursts/33TJ_20150614_232110_642_dng/",
"/Users/martinmarek/My Drive/Coding/Burst/bursts/Monika-HP-dng/",
"/Users/martinmarek/My Drive/Coding/Burst/bursts/Monika-stars-DNG/",
//"/Users/martinmarek/Downloads/rafs mini/",
"/Users/martinmarek/My Drive/Coding/Burst/bursts/Fuji X-trans RAF/",
// "/Users/martinmarek/My Drive/Coding/Burst/bursts/33TJ_20150614_232110_642_dng/",
// "/Users/martinmarek/My Drive/Coding/Burst/bursts/Monika-HP-dng/",
// "/Users/martinmarek/My Drive/Coding/Burst/bursts/Monika-stars-DNG/",
]

// iterate over bursts
for burst_dir in burst_dirs {

// load image paths for the burst
let fm = FileManager.default
let image_names = try fm.contentsOfDirectory(atPath: burst_dir).filter{$0.hasSuffix(".dng") || $0.hasSuffix(".DNG")}
var image_urls = image_names.map {URL(fileURLWithPath: burst_dir+$0)}
let burst_url = URL(fileURLWithPath: burst_dir)
var image_urls = try fm.contentsOfDirectory(at: burst_url, includingPropertiesForKeys: [], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
image_urls.sort(by: {$0.path < $1.path})

// ProcessingProgress is only useful for a GUI, but we have to instantiate one anyway
Expand All @@ -40,23 +42,9 @@ struct MyProgram {
let kernel_size = 5

// align+merge
let output_texture = try align_and_merge(image_urls: image_urls, progress: progress, ref_idx: ref_idx, search_distance: search_distance, tile_size: tile_size, kernel_size: kernel_size, robustness: robustness)

// set output image url
let in_url = image_urls[ref_idx]
let in_filename = in_url.deletingPathExtension().lastPathComponent
let out_filename = in_filename + "_merged"
let out_dir = NSHomeDirectory() + "/Pictures/Burst Photo/"
let out_path = out_dir + out_filename + ".dng"
let out_url = URL(fileURLWithPath: out_path)

// create output directory
if !FileManager.default.fileExists(atPath: out_dir) {
try FileManager.default.createDirectory(atPath: out_dir, withIntermediateDirectories: true, attributes: nil)
}
let out_url = try align_and_merge(image_urls: image_urls, progress: progress, ref_idx: ref_idx, search_distance: search_distance, tile_size: tile_size, kernel_size: kernel_size, robustness: robustness)
print("Image saved in:", out_url.relativePath)

// save the output image
try texture_to_dng(output_texture, in_url, out_url)
}

// terminate Adobe XMP SDK
Expand Down
2 changes: 2 additions & 0 deletions burstphoto/dng_utils/dng_sdk_wrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ enum ImageIOError: Error {
case save_error
}



func image_url_to_texture(_ url: URL, _ device: MTLDevice) throws -> (MTLTexture, Int) {

// read image
Expand Down
Loading

0 comments on commit 6fdfc02

Please sign in to comment.