Hi all, I have a python script I wrote to do this. I don't know if file attachments work for this mailing list, but let's find out.
Usage: If you want to look at focal length for all .jpg files in the current folder: $ python /path/to/PhotoAnalyze.py fl *.jpg It will generate a histogram. You can click and drag to define a range and it will write a summary file listing the file names of the images whose focal length fell within the range you dragged. For example, suppose I want to view a slide show of all the images taken at a focal length above 100 mm, I would click at 100 mm and drag off the right end of the histogram. Other properties you can plot aside from focalLength (fl) are: aperture, iso, shutterSpeed (ss). The little radio buttons on the left side of the histogram specify the dot extension for the file list that is written out. For example, I usually run the analysis on my .NEF files, but perhaps I want to look at a slideshow of all the .jpg files that correspond to the range I select. The script uses the extension ".jpg" for each of the file names in the list instead of ".NEF." I would very much like to write this sort of functionality into darktable, since then it could take advantage of darktable's powerful ability to generate collections based on arbitrary criteria (my script is most convenient if all the files you want to compare are in the same folder). Question: What would be the best thing to use to make this a darktable plugin? Lua? Does lua (or darktable) have some built-in concepts for histograms, for example? (I don't know if adapting darktable's exposure histogram is the best way to do this). Regards, Owen On Sat, Apr 16, 2016 at 2:36 PM, Rav <[email protected]> wrote: > Hi! > > It woulb be awesome to gave stats on your camera usage like how often you > use 16mm, iso100 or f/8. > > Best regards, > > Rav > Le 16 avr. 2016 19:18, <[email protected]> a écrit : > >> Hi, >> >> DT 2.0.3. >> >> Is there a simple way of getting stats/summary of focal >> lens used in a collection/film roll? >> >> or do I need to do that in exiftool? >> >> -- >> sknahT >> >> vyS >> >> ____________________________________________________________________________ >> darktable user mailing list >> to unsubscribe send a mail to >> [email protected] >> >> > ____________________________________________________________________________ > darktable user mailing list to unsubscribe send a mail to > [email protected] > ____________________________________________________________________________ darktable user mailing list to unsubscribe send a mail to [email protected]
# Copyright 2016 by Owen Mays # [email protected] # Provided as-is, no warranty of any kind. #!/usr/bin/python import time import os # for file I/O import sys # allows for an exit if wrong parameters are passed import exifread # for reading Exif's :-D import argparse # for parsing input arguments (including the file list) import matplotlib.pyplot as plt # plotting from matplotlib.widgets import RadioButtons #For radio buttons! import numpy as np # logs and logarithmic intervals (for ISO) from subprocess import call # for executing command-line commands #Line object for beginning and end of selection global startLine startLine=None global endLine endLine=None #File extension for summary file output global summaryFileExtension summaryFileExtension='jpg' parser = argparse.ArgumentParser() parser.add_argument("property",nargs=1,help="Specify the property to plot [focalLength (or fl), iso, ShutterSpeed (or ss), aperture]") parser.add_argument("files",nargs='+',help="Specify a list of files.") args = parser.parse_args() desiredData=np.array([]) #not using a default list b/c that would not allow element-wise math operations. keyErrorCounter=0 if args.property is None: print("Must specify a property") sys.exit() desiredProperty=args.property if(len(desiredProperty) != 1): print("Cannot specify more than one property to print") sys.exit() desiredProperty=desiredProperty[0].casefold() # more robust than casting to lowercase # Figure out which property was requested # Focal length if (desiredProperty=='fl' or desiredProperty=='focallength'): # Parameters for reading the EXIF stopTag='FocalLength' # stop reading the EXIF header after the fl is found. tagKey='EXIF FocalLength' #to get something out of the tags dictionary # Parameters for plotting title="Focal Lengths" xLabel="Focal Length [mm]" preUnits="" postUnits="mm" # to facilitate automatically printing "iso 400" or "35 mm" bins=range(17,202) # bin edges 17-201 inclusive. Range is exclusive, thus I specify 202 as the end point. xTicks=np.array(range(15,205,5)) xTickLabels=np.char.mod('%d',xTicks) xScale='linear' # ISO if (desiredProperty=='iso'): # Parameters for reading the EXIF stopTag='ISOSpeedRatings' # stop reading the EXIF header after the fl is found. tagKey='EXIF ISOSpeedRatings' #to get something out of the tags dictionary # Parameters for plotting title="ISO Values" xLabel="ISO Sensitivity" preUnits="iso" postUnits="" bins=np.array(range(0,8)) #these are going to be bin edges...and we're interested in 100*powers of 2 being evenly spaced (100,200,400,800, etc) bins=bins-.5 bins=100*2**bins xTicks=np.array([100,200,400,800,1600,3200,6400]) xTickLabels=np.char.mod('%d',xTicks) # gives an array of strings xScale='log' # shutter speed if (desiredProperty=='ss' or desiredProperty=='shutterspeed'): stopTag='ExposureTime' # this will be a ratio, and we want the denominator, watch out! tagKey='EXIF ExposureTime' title="Exposure Times" xLabel="Exposure Time [seconds]" preUnits="" postUnits="s" bins=np.logspace(0,3,num=100) xTicks=np.array([1,2,4,8,15,30,60,120,250,500,1000]) xTickLabels=np.char.mod('1/%d',xTicks) xScale='log' if (desiredProperty=='aperture'): stopTag='Aperture' tagKey='EXIF FNumber' #Ratios, but can be converted to ints. title='Aperture' xLabel="F-number" preUnits="f/" postUnits="" #Still not super happy with this spacing... bins= np.array([1,1.3,1.7,2,2.3,2.7,3,3.3,3.7,4,4.3,4.7,5,5.3,5.7,6,6.3,6.7,7,7.3,7.7,8,8.3]) #AV; f/N = sqrt(2**AV), from the wikipedia page on f/number bins=bins-.15; bins=np.sqrt((2**bins)) print(bins) xTicks=np.array([1.4,1.6,1.8,2.0,2.2,2.5,2.8,3.2,3.5,4,4.5,5,5.6,6.3,7.1,8,11,16]) xTickLabels=np.char.mod('f/%.1f',xTicks) xScale='log' for i, fileName in enumerate(args.files): #i is a dummy, enumerate returns a tuple f = open(fileName, 'rb') tags = exifread.process_file(f, details=False,stop_tag=stopTag) # EXIFread stores things in its own stupid Ratio datatype. # tags['EXIF FocalLength'] is the tag we want. # desiredData.values[0] is the ratio object holding the focal length. # ratio.num is the numerator, ratio.den is the denominator. # So all in one line, the following extracts numerator, denominator, does the division and appends that float to the desiredData list print(fileName) try: if (type(tags[tagKey].values[0]) is exifread.utils.Ratio): # if it's exifread's stupid ratio datatype if (stopTag=='ExposureTime'): #then we want the denominator desiredData=np.append(desiredData,float(tags[tagKey].values[0].den)) else: desiredData=np.append(desiredData,float(tags[tagKey].values[0].num/tags[tagKey].values[0].den)) elif (type(tags[tagKey].values[0]) is int): desiredData=np.append(desiredData,tags[tagKey].values[0]) else: print("Attribute was not a ratio or an int. Exiting.") sys.exit() except KeyError: keyErrorCounter+=1 # sometimes corrupted headers cause errors. I ignore and just count the failures, no sense breaking the analysis of a huge folder. print(keyErrorCounter," key errors (this many files were skipped).") fig = plt.figure() plt.hist(desiredData, bins=bins) histogramAxes=plt.axes() plt.title(title) plt.xlabel(xLabel) plt.ylabel("Count") plt.xscale(xScale) plt.xticks(xTicks,xTickLabels) # Note that I'm over-writing the x labels with strings to make the ISO display work. # This is dangerous! The values displayed on the x-axis are decoupled from the data! # If you screw up those strings, the values displayed at the tick marks have NOTHING to do with the data. plt.xlim(min(bins), max(bins)) # Avoid lots of white space between 0 and 100. # Add jpg/raw radio button buttonAx=plt.subplot(111) rax=plt.axes(plt.axes([0.01,0.2,.07,.07])) extensionRadio=RadioButtons(rax,('jpg','NEF','CR2')) #what to do when the extension radio button is clicked def changeExtension(label): global summaryFileExtension summaryFileExtension=label print(summaryFileExtension) extensionRadio.on_clicked(changeExtension) # plt.subplot(212) # plt.hist(desiredData,bins=(range(18,201)),normed=True,cumulative=True) # plt.xlabel("Focal Length [mm]") # plt.ylabel("Total below") # plt.xticks(range(15,200,5)) # Function to determine which bin a given coordinate belongs to def getBinLowerEdge(xCoord,bins): for binNum in range(0,len(bins)): if xCoord <= bins[binNum]: if xCoord>bins[binNum-1]: return (binNum-1) break else: print("error putting ",xCoord," into a bin") print("bins[binNum-1]=",bins[binNum-1]) print("bins[binNum]=",bins[binNum]) sys.exit() # Generate list of filenames between two points on the histogram def generateSummary(): global summaryFileExtension global xStartBin global xEndBin it=np.nditer(desiredData, flags=['f_index']) print("\n\n") #Generate a summary file name based on the selected range, e.g. 'SummaryFile_17-55mm.txt' or 'SummaryFile_ISO100-800.txt' summaryFileNameString='SummaryFile_{}{}-{}{}.txt'.format(preUnits.replace('/',''),str(bins[xStartBin]),str(bins[xEndBin]),postUnits) #If the summary file name exists, clear it: if(os.path.exists(summaryFileNameString)): open(summaryFileNameString,'w').close() summaryFile=open(summaryFileNameString,'w') print(summaryFileExtension) while not it.finished: if(bins[xStartBin] <= it[0] and it[0] <= bins[xEndBin]): #it[0] is the value in that location print(args.files[it.index],"=> ",preUnits,it[0],postUnits) summaryFile.write("{}.{}\n".format(os.path.splitext(args.files[it.index])[0],summaryFileExtension)) it.iternext() # Handle mouse clicks def on_click(event): global xStartBin global startLine global endLine global firstX #first x-coord clicked. user might want to drag right-to-left. if event.inaxes is histogramAxes: #Don't try to do anything with data processing if the user's clicking in the radio buttons. firstX=event.xdata if startLine is not None: startLine.set_xdata(event.xdata) else: startLine=histogramAxes.axvline(event.xdata,color='k') if endLine is not None: # this serves to "remove" the old end-line endLine.set_xdata(event.xdata) xStartBin = getBinLowerEdge(event.xdata,bins) plt.draw() else: print("Clicked outside axis bounds but inside plot window") def on_release(event): global firstX global xStartBin global xEndBin global startLine global endLine #Only do data processing and line drawing if the user clicks inside the histogram area. Radio buttons handle themselves. if event.inaxes is histogramAxes: xEndBin = getBinLowerEdge(event.xdata,bins)+1 if (event.xdata <= firstX): # user dragged right-to-left. Bin edge snapping needs to be flipped xEndBin=xEndBin-1 xStartBin=xStartBin+1 xStartBin, xEndBin = xEndBin, xStartBin #"start" should be at the lower value if endLine is not None: endLine.set_xdata(bins[xEndBin]) else: endLine=histogramAxes.axvline(bins[xEndBin],color='k') startLine.set_xdata(bins[xStartBin]) #make sure both lines snap to the edges of the bins plt.draw() generateSummary() # print("x dragged from ",bins[xStartBin]," to ",bins[xEndBin],"(bins ",xStartBin," to ",xEndBin,")") fig.canvas.callbacks.connect('button_press_event',on_click) fig.canvas.callbacks.connect('button_release_event',on_release) plt.show() plt.ion()
