Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
extract_wb.py
Go to the documentation of this file.
1#!/usr/bin/python3
2# This file is part of darktable,
3# Copyright (C) 2020-2021 Hubert Kowalski.
4#
5# darktable is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# darktable is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with darktable. If not, see <http://www.gnu.org/licenses/>.
17#
18#
19#
20#
21#
22#
23#
24#
25#
26# direct translation of extract_wb in python using as little external deps as possible
27#
28from __future__ import print_function
29import sys
30import os
31import xml.etree.ElementTree as ET
32import subprocess
33import shlex
34
35def eprint(*args, **kwargs):
36 print(*args, file=sys.stderr, **kwargs)
37
38if len(sys.argv) < 2 :
39 sys.exit("Usage: extract_wb <file1> [file2] ...")
40
41IGNORED_PRESETS = {"Auto", "Kelvin", "Measured", "AsShot", "As Shot", "Preset",
42 "Natural Auto", "Multi Auto", "Color Temperature Enhancement",
43 "One Touch WB 1", "One Touch WB 2", "One Touch WB 3",
44 "One Touch WB 4", "Custom WB 1", "Auto0", "Auto1", "Auto2",
45 "Custom", "CWB1", "CWB2", "CWB3", "CWB4", "Black",
46 "Illuminator1", "Illuminator2", "Uncorrected"}
47
48FL_PRESET_REPLACE = {
49 "Fluorescent" : "CoolWhiteFluorescent",
50 "FluorescentP1" : "DayWhiteFluorescent",
51 "FluorescentP2" : "DaylightFluorescent",
52 "FluorescentM1" : "WarmWhiteFluorescent",
53 "FluorescentD" : "DaylightFluorescent",
54 "FluorescentN" : "NeutralFluorescent",
55 "FluorescentW" : "WhiteFluorescent",
56 "Daylight Fluorescent" : "DaylightFluorescent",
57 "Day White Fluorescent" : "DayWhiteFluorescent",
58 "White Fluorescent" : "WhiteFluorescent",
59 "Unknown (0x600)" : "Underwater",
60 "Sunny" : "DirectSunlight",
61 "Fine Weather" : "DirectSunlight",
62 "Tungsten (Incandescent)" : "Tungsten",
63 "ISO Studio Tungsten" : "Tungsten",
64 "Cool WHT FL" : "CoolWhiteFluorescent",
65 "Daylight FL" : "DaylightFluorescent",
66 "Warm WHT FL" : "WarmWhiteFluorescent",
67 "Warm White Fluorescent" : "WarmWhiteFluorescent",
68 "White FL" : "WhiteFluorescent",
69 "Mercury Lamp" : "HighTempMercuryVaporFluorescent",
70 "Day White FL" : "DayWhiteFluorescent",
71 "Sodium Lamp" : "SodiumVaporFluorescent",
72 "3000K (Tungsten light)" : "Tungsten",
73 "4000K (Cool white fluorescent)" : "CoolWhiteFluorescent",
74 "5300K (Fine Weather)" : "Daylight",
75 "5500K (Flash)" : "Flash",
76 "6000K (Cloudy)" : "Cloudy",
77 "7500K (Fine Weather with Shade)" : "Shade",
78 }
79
80PRESET_ORDER = ["DirectSunlight", "Daylight", "D55", "Shade","Cloudy",
81 "Tungsten", "Incandescent","Fluorescent",
82 "WarmWhiteFluorescent", "CoolWhiteFluorescent",
83 "DayWhiteFluorescent","DaylightFluorescent",
84 "DaylightFluorescent", "NeutralFluorescent", "WhiteFluorescent",
85 "HighTempMercuryVaporFluorescent", "HTMercury",
86 "SodiumVaporFluorescent", "Underwater", "Flash", "Unknown"]
87
88PRESET_SORT_MAPPING = {}
89
90for index,name in enumerate(PRESET_ORDER):
91 PRESET_SORT_MAPPING[name] = index + 1
92
93cams_from_source = os.path.dirname(os.path.abspath(__file__)) + "/../src/external/rawspeed/data/cameras.xml"
94cams_from_dist = os.path.dirname(os.path.abspath(__file__)) + "/../rawspeed/cameras.xml"
95
96CAMERAS = os.path.abspath(cams_from_source) if os.path.exists(os.path.abspath(cams_from_source)) else os.path.abspath(cams_from_dist)
97
98if not os.path.exists(CAMERAS):
99 sys.exit("Can't find cameras mapping file, should be in {0}".format(CAMERAS))
100
101exif_name_map = {}
102xml_doc = ET.parse(CAMERAS)
103for camera in xml_doc.getroot().findall('Camera'):
104 maker = exif_maker = camera.get('make')
105 model = exif_model = camera.get('model')
106 exif_id = maker,model
107 if camera.find('ID') is not None:
108 cid = camera.find('ID')
109 maker = cid.get('make')
110 model = cid.get('model')
111 exif_name_map[exif_id] = maker,model
112 for alias in camera.findall('Aliases/Alias'):
113 exif_model = alias.text
114 exif_id = exif_maker, exif_model
115 exif_name_map[exif_id] = maker,model
116
117found_presets = []
118
119for filename in sys.argv[1:]:
120 red = green = blue = maker = model = preset = None
121 finetune = fl_count = rlevel = blevel = glevel = 0
122 listed_presets = []
123 preset_names = {}
124 gm_skew = False
125 command = "exiftool -Make -Model \"-WBType*\" \"-WB_*\" \"-ColorTemp*\" "\
126 "-WhiteBalance -WhiteBalance2 -WhitePoint -ColorCompensationFilter "\
127 "-WBShiftAB -WBShiftAB_GM -WBShiftAB_GM_Precise -WBShiftGM -WBScale "\
128 "-WhiteBalanceFineTune -WhiteBalanceComp -WhiteBalanceSetting "\
129 "-WhiteBalanceBracket -WhiteBalanceBias -WBMode -WhiteBalanceMode "\
130 "-WhiteBalanceTemperature -WhiteBalanceDetected -ColorTemperature "\
131 "-WBShiftIntelligentAuto -WBShiftCreativeControl -WhiteBalanceSetup "\
132 "-WBRedLevel -WBBlueLevel -WBGreenLevel -RedBalance -BlueBalance "\
133 "\"{0}\"".format(filename)
134 if filename.endswith(('.txt','.TXT')):
135 command = 'cat "{0}"'.format(filename)
136 command = shlex.split(command)
137 proc = subprocess.check_output(command, universal_newlines=True)
138 for io in proc.splitlines():
139 lineparts = io.split(':')
140 tag = lineparts[0].strip()
141 values = lineparts[1].strip().split(' ')
142 if 'Make' in tag.split():
143 maker = lineparts[1].strip()
144 elif 'Model' in tag.split():
145 model = lineparts[1].strip()
146 elif tag == "WB RGGB Levels":
147 green = (float(values[1])+float(values[2]))/2.0
148 red = float(values[0])/green
149 blue = float(values[3])/green
150 green = 1
151 elif tag == "WB RB Levels":
152 red = float(values[0])
153 blue = float(values[1])
154 if len(values) == 4 and values[2] == "256" and values[3] == "256":
155 red /= 256.0
156 blue /= 256.0
157 green = 1
158 elif tag == "WB GRB Levels":
159 green = float(values[0])
160 red = float(values[1])/green
161 blue = float(values[2])/green
162 green = 1
163 # elif tag == "WB GRB Levels Auto" and maker == "FUJIFILM" # fuji seems to use "WB GRB Levels Auto to describe manual finetuning
164 # green = float(values[0])
165 # red = float(values[1])/green
166 # blue = float(values[2])/green
167 # green = 1
168 elif tag == "White Point" and len(values) > 3:
169 green = (float(values[1])+float(values[2]))/2.0
170 red = float(values[0])/green
171 blue = float(values[3])/green
172 green = 1
173 elif tag == "White Balance" or tag == "White Balance 2":
174 preset = ' '.join(values)
175 if preset in FL_PRESET_REPLACE:
176 preset = FL_PRESET_REPLACE[preset]
177 elif ' '.join(tag.split()[:2]) == "WB Type":
178 preset_names[' '.join(tag.split()[2:])] = ' '.join(values)
179 elif ' '.join(tag.split()[:3]) in ['WB RGB Levels', 'WB RGGB Levels', 'WB RB Levels']:
180 # todo - this codepath is weird
181 p = ''.join(tag.split()[3:])
182 if( p in preset_names):
183 p = preset_names[p]
184
185 r=g=b=0
186
187 if len(values) == 4 and ' '.join(tag.split()[:3]) in ['WB RB Levels']:
188 g = (float(values[2])+float(values[3]))/2.0
189 r = float(values[0])/g
190 b = float(values[1])/g
191 g = 1
192 elif len(values) == 4:
193 g = (float(values[1])+float(values[2]))/2.0
194 r = float(values[0])/g
195 b = float(values[3])/g
196 g = 1
197 elif len(values) == 3:
198 g = float(values[1])
199 r = float(values[0])/g
200 b = float(values[2])/g
201 g = 1
202 elif len(values) == 2 and ' '.join(tag.split()[:3]) in ['WB RB Levels']:
203 r = float(values[0])
204 b = float(values[2])
205 g = 1
206 else:
207 eprint("Found RGB tag '{0}' with {1} values instead of 2, 3 or 4".format(p, len(values)))
208
209 if 'Fluorescent' in p:
210 fl_count += 1
211
212 if not p:
213 p= 'Unknown'
214 if p not in IGNORED_PRESETS:
215 listed_presets.append(tuple([p,r,g,b]))
216 elif tag == "WB Red Level":
217 rlevel = float(values[0])
218 elif tag == "WB Blue Level":
219 blevel = float(values[0])
220 elif tag == "WB Green Level":
221 glevel = float(values[0])
222 elif tag == "WB Shift AB": # canon - positive is towards amber, panasonic/leica/pentax - positive is towards blue?
223 finetune = values[0]
224 elif tag == "WB Shift GM": # detect GM shift and warn about it
225 gm_skew = gm_skew or (int(values[0]) != 0)
226 elif tag == "WB Shift AB GM": # Sony
227 finetune = values[0]
228 gm_skew = gm_skew or (int(values[1]) != 0)
229 elif tag == "WB Shift AB GM Precise" and maker.startswith("SONY"): # Sony
230 finetune = int(float(values[0]) * 2.0)
231 gm_skew = gm_skew or (float(values[1]) != 0.0)
232 elif tag == "White Balance Fine Tune" and maker.startswith("NIKON"): # nikon
233 finetune = 0-(int(values[0]) * 2) # nikon lies about half-steps (eg 6->6->5 instead of 6->5.5->5, need to address this later on, so rescalling this now)
234 gm_skew = gm_skew or (int(values[1]) != 0)
235 elif tag == "White Balance Fine Tune" and maker == "FUJIFILM" and int(values[3]) != 0: # fuji
236 eprint("Warning: Fuji does not seem to produce any sensible data for finetuning! If all finetuned values are identical, use one with no finetuning (0)")
237 finetune = int(values[3]) / 20 # Fuji has -180..180 but steps are every 20
238 gm_skew = gm_skew or (int(values[1].replace(',','')) != 0)
239 elif tag == "White Balance Fine Tune" and maker == "SONY" and preset == "CoolWhiteFluorescent":
240 # Sony's Fluorescent Fun
241 if values[0] == "-1":
242 preset = "WarmWhiteFluorescent"
243 elif values[0] == "0":
244 preset = "CoolWhiteFluorescent"
245 elif values[0] == "1":
246 preset = "DayWhiteFluorescent"
247 elif values[0] == "2":
248 preset = "DaylightFluorescent"
249 else:
250 eprint("Warning: Unknown Sony Fluorescent WB Preset!")
251 elif tag == "White Balance Bracket": # olympus
252 finetune = values[0]
253 gm_skew = gm_skew or (int(values[1]) != 0)
254 elif tag == "Color Compensation Filter": # minolta?
255 gm_skew = gm_skew or (int(values[0]) != 0)
256
257 if rlevel > 0 and glevel > 0 and blevel > 0:
258 red = rlevel/glevel
259 blue = blevel/glevel
260 green = 1
261
262 if gm_skew:
263 eprint('WARNING: {0} has finetuning over GM axis! Data is skewed!'.format(filename))
264
265 # Adjust the maker/model we found with the map we generated before
266 if exif_name_map[maker,model]:
267 enm = exif_name_map[maker,model]
268 maker = enm[0]
269 model = enm[1]
270 else:
271 eprint("WARNING: Couldn't find model in cameras.xml ('{0}', '{1}')".format(maker, model))
272
273 for preset_arr in listed_presets:
274 # ugly hack. Canon's Fluorescent is listed as WhiteFluorescent in usermanual
275 preset_arrv = list(preset_arr)
276 if maker and maker == "Canon" and preset_arrv[0] == "Fluorescent":
277 preset_arrv[0] = "WhiteFluorescent"
278 if preset_arrv[0] in FL_PRESET_REPLACE:
279 preset_arrv[0] = FL_PRESET_REPLACE[preset_arrv[0]]
280 if preset_arrv[0] not in IGNORED_PRESETS:
281 found_presets.append(tuple([maker,model,preset_arrv[0], 0, preset_arrv[1], preset_arrv[2], preset_arrv[3]]))
282
283 # Print out the WB value that was used in the file
284 if not preset:
285 preset = filename
286 if red and green and blue and preset not in IGNORED_PRESETS:
287 found_presets.append(tuple([maker, model, preset, int(finetune), red, green, blue]))
288
289# get rid of duplicate presets
290
291found_presets = list(set(found_presets))
292
293def preset_to_sort(preset):
294 sort_for_preset = 0
295 if preset[2] in IGNORED_PRESETS:
296 sort_for_preset = 0
297 elif preset[2] in PRESET_SORT_MAPPING:
298 sort_for_preset = PRESET_SORT_MAPPING[preset[2]]
299 elif preset[2].endswith('K'):
300 sort_for_preset = int(preset[2][:-1])
301 else:
302 eprint("WARNING: no defined sort order for '{0}'".format(preset[2]))
303 return tuple([preset[0], preset[1], sort_for_preset, preset[3], preset[4], preset[5], preset[6]])
304
305found_presets.sort(key=preset_to_sort)
306
307min_padding = 0
308for preset in found_presets:
309 if len(preset[2]) > min_padding:
310 min_padding = len(preset[2])
311
312#dealing with Nikon half-steps
313for index in range(len(found_presets)-1):
314 if (found_presets[index][0] == 'Nikon' and #case now translated
315 found_presets[index+1][0] == found_presets[index][0] and
316 found_presets[index+1][1] == found_presets[index][1] and
317 found_presets[index+1][2] == found_presets[index][2] and
318 found_presets[index+1][3] == found_presets[index][3]) :
319
320 curr_finetune = int(found_presets[index][3])
321
322 if curr_finetune < 0:
323 found_presets[index+1] = list(found_presets[index+1])
324 found_presets[index+1][3] = (int(found_presets[index+1][3]) + 1)
325 found_presets[index+1] = tuple(found_presets[index+1])
326 elif curr_finetune > 0:
327 found_presets[index] = list(found_presets[index])
328 found_presets[index][3] = (curr_finetune) - 1
329 found_presets[index] = tuple(found_presets[index])
330
331# check for gaps in finetuning for half-steps (seems that nikon and sony can have half-steps)
332for index in range(len(found_presets)-1):
333 if ( (found_presets[index][0] == "Nikon" or found_presets[index][0] == "Sony") and #case now translated
334 found_presets[index+1][0] == found_presets[index][0] and
335 found_presets[index+1][1] == found_presets[index][1] and
336 found_presets[index+1][2] == found_presets[index][2]) :
337
338 found_presets[index] = list(found_presets[index])
339 found_presets[index+1] = list(found_presets[index+1])
340
341 if (found_presets[index+1][3] % 2 == 0 and
342 found_presets[index][3] % 2 == 0 and
343 found_presets[index+1][3] == found_presets[index][3] + 2):
344
345 #detected gap eg -12 -> -10. slicing in half to undo multiplication done earlier
346 found_presets[index][3] = int(found_presets[index][3] / 2)
347 found_presets[index+1][3] = int(found_presets[index+1][3] / 2)
348 elif (found_presets[index+1][3] % 2 == 0 and
349 found_presets[index][3] % 2 == 1 and
350 found_presets[index+1][3] == (found_presets[index][3] + 1)*2 and
351 (index + 2 == len(found_presets) or
352 found_presets[index+2][2] != found_presets[index+1][2] ) ):
353
354 #dealing with corner case of last-halfstep not being dealth with earlier
355 found_presets[index+1][3] = int(found_presets[index+1][3] / 2)
356
357 found_presets[index] = tuple(found_presets[index])
358 found_presets[index+1] = tuple(found_presets[index+1])
359
360#detect lazy finetuning (will not complain if there's no finetuning)
361lazy_finetuning = []
362for index in range(len(found_presets)-1):
363 if (found_presets[index+1][0] == found_presets[index][0] and
364 found_presets[index+1][1] == found_presets[index][1] and
365 found_presets[index+1][2] == found_presets[index][2] and
366 found_presets[index+1][3] != ((found_presets[index][3])+1) ):
367
368 # found gap. complain about needing to interpolate
369 lazy_finetuning.append(tuple([found_presets[index][0], found_presets[index][1], found_presets[index][2]]))
370
371# Get rid of duplicate lazy finetuning reports
372lazy_finetuning = list(set(lazy_finetuning))
373
374# $stderr.puts lazy_finetuning.inspect.gsub("], ", "],\n") # debug content
375
376for lazy in lazy_finetuning:
377 eprint("Gaps detected in finetuning for {0} {1} preset {2}, dt will need to interpolate!".format(lazy[0], lazy[1], lazy[2]))
378
379for preset in found_presets:
380 if preset[2] in IGNORED_PRESETS:
381 eprint("Ignoring preset '{0}'".format(preset[2]))
382 else:
383 preset_name = ''
384 if preset[2].endswith('K'):
385 preset_name = '"'+preset[2]+'"'
386 else:
387 preset_name = preset[2]
388 print(' {{ "{0}", "{1}", {2:<{min_pad}}, {3}, {{ {4}, {5}, {6}, 0 }} }},'.format(preset[0], preset[1], preset_name, preset[3], preset[4], preset[5], preset[6], min_pad=min_padding))
if(L< 0.5f) C
eprint(*args, **kwargs)
Definition extract_wb.py:35
preset_to_sort(preset)