Python can be used to analyse your normal map and reconstruct the bump map and this can then be used to generate a curvature map.
Here's the code to run - simply paste this into a Text Editor window, load your image into the Image Editor, ensure it has '_nmp' or '.nmp' somewhere in the filename and click 'Run Script'.
import bpy
import os
import math
pixels = None
width = None
height = None
def get_pixel(x, y):
global pixels, width, height
base = (y*width+x)*4
return (pixels[base], pixels[base+1], pixels[base+2], pixels[base+3])
def set_pixel(x, y, rgba):
global pixels, width, height
base = (y*width+x)*4
pixels[base] = rgba[0]
pixels[base+1] = rgba[1]
pixels[base+2] = rgba[2]
pixels[base+3] = rgba[3]
#Adapted from http://blender.stackexchange.com/a/80047/29586
def srgb_to_linear(c):
a = 0.055
if c <= 0.04045:
return c / 12.92
else:
return ((c+a) / (1+a) ** 2.4)
# RGBA was previously held as 8-bit. Re-normalize to correct for 8-bit innacurracies
def normalize_normal(rgba):
dist = math.sqrt(math.pow(rgba[0],2) + math.pow(rgba[1],2) + math.pow(rgba[2],2))
if (dist > 0):
return (rgba[0]/dist, rgba[1]/dist, rgba[2]/dist, rgba[3])
else:
return (0,0,0,0)
def calc_gradients(normal_x,normal_y):
adj_x = (normal_x - 0.5)*2
adj_y = (normal_y - 0.5)*2
# Filter out the extremes to avoid infinities
if adj_x < -0.99:
adj_x = -0.99
if adj_x > 0.99:
adj_x = 0.99
if adj_y < -0.99:
adj_y = -0.99
if adj_y > 0.99:
adj_y = 0.99
return (math.tan(math.asin(adj_x))/100, math.tan(math.asin(adj_y))/100)
def normalize(arr, width, height):
#Get range of depth
max_depth = -99999999999999999999
min_depth = 9999999999999999999
for x in range(0,width):
#print("Normalizing(1)... %i" % x)
for y in range(0,height):
depth = arr[y*width+x]
if depth < min_depth:
min_depth = depth
if depth > max_depth:
max_depth = depth
print("max = %f, min = %f" % (max_depth, min_depth))
if max_depth > 1000:
print("Limited max to 1000")
max_depth = 1000
if min_depth < -1000:
print("Limited min to -1000")
min_depth = -1000
if max_depth == min_depth:
max_depth = min_depth + 1
#Normalize depth to 0.0 to 1.0 range
for x in range(0,width):
#print("Normalizing(2)... %i" % x)
for y in range(0,height):
depth = (arr[y*width+x] - min_depth) / (max_depth - min_depth)
#print(depth)
arr[y*width+x] = depth
def generate_curviness(heights, width, height, radius):
#determine curviness at each point and store in RED channel
curviness = [0.0] * width * height
print("Calculating Curviness (%i)..." % radius)
for x in range(1,width-1):
for y in range(1,height-1):
for d in range(1,radius+1):
if (x-d)<0 or (x+d)>=width or (y-d)<0 or (y+d)>=height:
pass
else:
h = heights[y*width+x]
hv1 = heights[(y-d)*width+x]
hv2 = heights[(y+d)*width+x]
hh1 = heights[y*width+x-d]
hh2 = heights[y*width+x+d]
if hv1 < hv2:
vcurviness = (h - hv2) - (hv1 - h)
else:
vcurviness = (h - hv1) - (hv2 - h)
if hh1 < hh2:
hcurviness = (h - hh2) - (hh1 - h)
else:
hcurviness = (h - hh1) - (hh2 - h)
curviness[y*width+x] += (vcurviness + hcurviness)/(d*d)
return curviness
def process_image(image_name, new_image_name):
global pixels, width, height
image = bpy.data.images[image_name]
width = image.size[0]
height = image.size[1]
# The source image pixels - stream of R,G,B,A for each pixel, width x height
pixels = list(image.pixels)
# Gradient from each pixel in each direction - up, left, right, down
gradient_u = [float] * width * height
gradient_l = [float] * width * height
gradient_r = [float] * width * height
gradient_d = [float] * width * height
# Determine gradient in each direction for each pixel
for x in range(0,width):
print("Capture gradients - Column %i of %i" % (x,width))
for y in range(0,height):
this_rgba = normalize_normal(get_pixel(x, y))
this_rgba_gradient = calc_gradients(this_rgba[0], this_rgba[1])
if x == 0:
left_rgba = None
left_rgba_gradient = (0,0)
else:
left_rgba = normalize_normal(get_pixel(x-1, y))
left_rgba_gradient = calc_gradients(left_rgba[0], left_rgba[1])
if y == 0:
bottom_rgba = None
bottom_rgba_gradient = (0,0)
else:
bottom_rgba = normalize_normal(get_pixel(x, y-1))
bottom_rgba_gradient = calc_gradients(bottom_rgba[0], bottom_rgba[1])
if x == (width-1):
right_rgba = None
right_rgba_gradient = (0,0)
else:
right_rgba = normalize_normal(get_pixel(x+1, y))
right_rgba_gradient = calc_gradients(right_rgba[0], right_rgba[1])
if y == (height-1):
top_rgba = None
top_rgba_gradient = (0,0)
else:
top_rgba = normalize_normal(get_pixel(x, y+1))
top_rgba_gradient = calc_gradients(top_rgba[0], top_rgba[1])
# Average each directional gradient with this one to give the gradient between the pixels in each direction
gradient_u[y*width+x] = (this_rgba_gradient[1] + top_rgba_gradient[1])/2
gradient_d[y*width+x] = -(this_rgba_gradient[1] + bottom_rgba_gradient[1])/2
gradient_l[y*width+x] = -(this_rgba_gradient[0] + left_rgba_gradient[0])/2
gradient_r[y*width+x] = (this_rgba_gradient[0] + right_rgba_gradient[0])/2
# Store it
ent_u[y*width+x]))
set_pixel(x, y, (this_rgba[0], this_rgba[1], this_rgba[2], gradient_u[y*width+x]))
# Now start with 0's and process the whole image pixel by pixel averaging the resulting heights generated by the surrounding pixels
heights = [0.0] * width * height
numpasses = int(max(width, height)/2)
for p in range(0,numpasses):
print("Pass %i of %i" % (p, numpasses))
for x in range(0,width):
for y in range(0,height):
if y < (height-1):
height_top = heights[(y+1)*width+x]
else:
height_top = 0.0
if y > 0:
height_bottom = heights[(y-1)*width+x]
else:
height_bottom = 0.0
if x < (width-1):
height_right = heights[y*width+x+1]
else:
height_right = 0.0
if x > 0:
height_left = heights[y*width+x-1]
else:
height_left = 0.0
baseindex = y*width+x
height_this = heights[baseindex]
#Calculate and store the new height
heights[y*width+x] = (height_this + (height_top + gradient_u[baseindex]) + (height_bottom + gradient_d[baseindex]) + (height_left + gradient_l[baseindex]) + (height_right + gradient_r[baseindex])) / 5
normalize(heights, width, height)
print("Storing Bump...")
for x in range(0,width):
for y in range(0,height):
pixels[(y*width+x)*4+3] = heights[y*width+x]
# Generate Red with radius 1
curviness = generate_curviness(heights, width, height, 1)
normalize(curviness, width, height)
print("Storing Curviness...")
for x in range(0,width):
for y in range(0,height):
pixels[(y*width+x)*4+0] = curviness[y*width+x]
# Generate Green with radius 5
curviness = generate_curviness(heights, width, height, 5)
normalize(curviness, width, height)
print("Storing Curviness...")
for x in range(0,width):
for y in range(0,height):
pixels[(y*width+x)*4+1] = curviness[y*width+x]
# Generate Blue with radius 25
curviness = generate_curviness(heights, width, height, 25)
normalize(curviness, width, height)
print("Storing Curviness...")
for x in range(0,width):
for y in range(0,height):
pixels[(y*width+x)*4+2] = curviness[y*width+x]
print("Storing image...")
new_image = bpy.data.images.new(new_image_name, width=width, height=height)
new_image.pixels = pixels[:]
new_image.update()
new_image.pack(as_png=True)
new_image.use_fake_user = True
print("Done")
for img in bpy.data.images:
if "_nmp" in img.name or ".nmp" in img.name:
new_img_name = img.name
new_img_name = new_img_name.replace('nmp', 'bump')
print("%s -> %s" % (img.name, new_img_name))
process_image(img.name, new_img_name)
The script will look for all images with '_nmp' or '.nmp' in the name and will generate an image of the same name, but with 'nmp' replaced with 'bump'. Each generated image will consist of 4 channels (RGBA). The Alpha channel will be a bump map generated from the normals, while the Red, Green and Blue channels are each a Curvature map - one with a radius of 1 pixels (Red channel), one with a radius of 5 pixels (Green channel) and one with a radius of 25 pixels. The curvature is measured only in the Horizontal and Vertical directions but the result seem reasonable to my eye (I've not got much experience of curvature maps - if there's a problem with these then let me know and I can update the calculations).
Here's the normal map (a section of the map from your question) :

Here's the result :

Bottom-right is the generated bump map, Bottom-left is the red channel (1 pixel radius curvature), top-right is the green channel (5 pixel radius curvature), top-left if the blue channel (25 pixel radius curvature).
The way this works is to first process the normals from the normal map to determine the horizontal and vertical gradients at each point. These are re-normalized to correct for any inaccuracies (such as 8-bit encoding) and extreme values are clipped to avoid problems before being averaged with its neighbours to get a gradient between those pixels. These gradients are then used to determine the difference in height between neighbouring pixels and those height differences built up into the bump map (this is the part that takes the time).
The resulting bump map is stored in the alpha channel and is then sampled to determine the curvature at each point, storing each in the R,G,B channels.
The script produces output of progress to the console - so run Blender from the command line to view the output. Beware large normal maps taking a considerable amount of time to process - on my system the full 2048x2048 map was estimates to take around 10 hours to complete so I used a cut down section for my testing (604x408 pixels) - which only took a few minutes to complete.
EDIT :
Just to clarify, the script takes the original Normal Map...
'rock5_nmp_clipped'

... and extracts the height information into a bump map with the same name but with 'nmp' replaced with 'bump'...
'rock5_bump_clipped'

...along with 3 separate channels, each with the curviness map calculated at different radii :

To analyse curvature using a different radius simply amend the calls to the 'generate_curviness(...)' function and re-run the script to generate a new image :
#Use radius of 25
curviness = generate_curviness(heights, width, height, 25)
e.g., change to :
#Use radius of 40
curviness = generate_curviness(heights, width, height, 40)
Using the Normal Map

The curviness stored in the R,G,B channels of the generated bump map can be used to add wear or additional effects :

Since we now have the actual underlying 'bump' information we can use this to drive other effects - such as the True Displacement - generating a much more realistic surface :

(Above example rendered using a single plane, subdivided to 256x256 faces and displaced with True Displacement (see displacement properties panel at bottom-right of image). All depth, displacement, and wear derived from original normal map, extracted by script.)