Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b8ee32e
implemented image stack, without testing yet
amirDahari1 Dec 19, 2024
33c2932
cls =0 bug fix from random pixels
amirDahari1 Dec 20, 2024
bfe126d
tests for batch images
amirDahari1 Dec 20, 2024
383a618
error handling in phase frac display; prevent divide by 0
Jan 15, 2025
225dc1a
potentially fixed rgba tiff/png problem: convert the image to greysca…
Jan 16, 2025
812ce67
link to preprint in menus + readme, update citation.cff
Jan 16, 2025
79a19f5
linked to github & issues page in model info
Jan 16, 2025
e4862bd
added further explanation to opening menu
Jan 17, 2025
dfec5f2
Update DragDrop.tsx
amirDahari1 Feb 10, 2025
e112d57
Update DragDrop.tsx
amirDahari1 Feb 10, 2025
0875f9a
Merge pull request #53 from tldr-group/bug-fixes
amirDahari1 Feb 10, 2025
3ed448d
Merge pull request #52 from tldr-group/feature-image-stack
amirDahari1 Mar 25, 2025
4b67afb
add imagecodecs package to allow LZW compressed stacks
May 5, 2025
0b1bdd9
made fonts larger
amirDahari1 May 12, 2025
8c59157
added a confidence interval flag for different confidences
amirDahari1 May 12, 2025
936241d
figure legend changed
amirDahari1 May 28, 2025
b38eb57
3 decimal places in the results text, maybe solves #49
amirDahari1 May 28, 2025
d61391b
Merge pull request #59 from tldr-group/review-figure-changes
amirDahari1 May 28, 2025
b9a254c
Merge branch 'bug-fixes' into development
May 28, 2025
cc78492
tighter bounds test
amirDahari1 May 30, 2025
f9ccc00
adds a test of comparing to classical approach only for small imags
amirDahari1 May 30, 2025
73bdabb
Merge pull request #61 from tldr-group/feature-when-to-test-classical
amirDahari1 May 30, 2025
baf6e61
changed minimum size for classical comparison
amirDahari1 May 30, 2025
b49c635
And text changed
amirDahari1 May 30, 2025
0be127a
Typo
amirDahari1 May 30, 2025
abce278
Update tests.yaml
amirDahari1 May 30, 2025
6a16f83
Merge branch 'development' of https://github.com/tldr-group/Represent…
May 30, 2025
6e4483f
Update tests.yaml
amirDahari1 Jun 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
push:
branches: [ "cleanup" ]
pull_request:
branches: [ "main", ]
branches: [ "main", "development" ]

permissions:
contents: read
Expand Down
6 changes: 3 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ authors:
- name: Samuel J. Cooper
orcid: 0000-0003-4055-6903
title: "Prediction of Microstructural Representativity from a Single Image"
doi: ARXIV_DOI
url: "https://github.com/tldr-group/ImageRep"
doi: arXiv:2410.19568v1
url: "https://arxiv.org/abs/2410.19568v1"
preferred-citation:
type: article
authors:
Expand All @@ -23,7 +23,7 @@ preferred-citation:
orcid: 0000-0002-7263-6724
- name: Samuel J. Cooper
orcid: 0000-0003-4055-6903
doi: ARXIV_DOI
doi: arXiv:2410.19568v1
journal: "arXiV preprint"
month: 8
title: "Prediction of Microstructural Representativity from a Single Image"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[Try it out!](https://www.imagerep.io/)

Here we introduce the 'ImageRep' method for fast phase fraction representativity estimation from a single microstructural image. This is achieved by calculating the Two-Point Correlation (TPC) function of the image, combined with a data-driven analysis of the [MicroLib](https://microlib.io/) dataset. By applying a statistical framework that utilizes both data sources, we can establish the uncertainty in the phase fraction in the image with a given confidence, **and** the image size that would be needed to meet a given target uncertainty. Further details are provided in our [paper](CITATION.cff).
Here we introduce the 'ImageRep' method for fast phase fraction representativity estimation from a single microstructural image. This is achieved by calculating the Two-Point Correlation (TPC) function of the image, combined with a data-driven analysis of the [MicroLib](https://microlib.io/) dataset. By applying a statistical framework that utilizes both data sources, we can establish the uncertainty in the phase fraction in the image with a given confidence, **and** the image size that would be needed to meet a given target uncertainty. Further details are provided in our [preprint](https://arxiv.org/abs/2410.19568).

If you use this ImageRep in your research, [please cite us](CITATION.cff).

Expand Down
7 changes: 5 additions & 2 deletions frontend/src/components/DragDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ const DragDrop = ({ loadFromFile }: DragDropProps): JSX.Element => {
onDragOver={handleDrag}
onDrop={handeDrop}
>
{(!isMobile) && <span>Drag microstructure file, or view example <a style={{ cursor: "pointer", color: 'blue' }} onClick={e => viewExample(DEFAULT_IMAGE_2D)}>in 2D</a> or <a style={{ cursor: "pointer", color: 'blue' }} onClick={e => viewExample(DEFAULT_IMAGE_3D)}> 3D</a></span>}
{(!isMobile) && <div style={{display: 'flex', alignItems: 'center', flexDirection: 'column'}}>
<span>Drag microstructure file, or view example <a style={{ cursor: "pointer", color: 'blue' }} onClick={e => viewExample(DEFAULT_IMAGE_2D)}>in 2D</a> or <a style={{ cursor: "pointer", color: 'blue' }} onClick={e => viewExample(DEFAULT_IMAGE_3D)}> 3D</a></span>
<span style={{fontSize: '0.7em'}}>(image must be segmented & image background (e.g. scale bar) cropped out)</span>
</div>}
</div>
);
}

export default DragDrop
export default DragDrop
45 changes: 28 additions & 17 deletions frontend/src/components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,34 @@ const ConfidenceSelect = () => {


const vals = imageInfo?.phaseVals!

console.log(accurateFractions)
console.log(vals)
console.log(selectedPhase)

const displayPhaseFrac = () => {
try {
if (accurateFractions == null && imageInfo && vals[selectedPhase -1] && imageInfo?.previewData.data) {
console.log('help')
Comment thread
amirDahari1 marked this conversation as resolved.
const pf = getPhaseFraction(
imageInfo?.previewData.data!,
vals[selectedPhase - 1]
)
if (typeof pf == "number") {
return pf.toFixed(3)
}
} else if (accurateFractions && vals[selectedPhase -1]) {
return accurateFractions[vals[selectedPhase - 1]].toFixed(3)
}

return ''
} catch (e) {
return ''
}
}
// horrible ternary: if server has responded and set the accurate phase fractions,
// then use those values in the modal. If not, use the estimate from the first image
const phaseFrac = (accurateFractions != null) ?
accurateFractions[vals[selectedPhase - 1]].toFixed(3)
: getPhaseFraction(
imageInfo?.previewData.data!,
vals[selectedPhase - 1]
).toFixed(3);
const phaseFrac = displayPhaseFrac()

const setConf = (e: any) => {
setSelectedConf(Number(e.target!.value))
Expand Down Expand Up @@ -236,13 +256,6 @@ const Result = () => {
setMenuState('processing');
}

const getDPofSigFig = (decimal: number) => {
const rounded = parseFloat(decimal.toPrecision(1));
const loc = Math.ceil(Math.abs(Math.log10(rounded)));
const capped = Math.min(loc, 5)
return capped
}

const c = colours[selectedPhase];
const headerHex = rgbaToHex(c[0], c[1], c[2], c[3]);

Expand All @@ -255,10 +268,8 @@ const Result = () => {
const absErrFromPFB = (pfB![1] - pfB![0]) / 2
const perErrFromPFB = 100 * (((pfB![1] - pfB![0]) / 2) / phaseFrac)

const roundTo = getDPofSigFig(absErrFromPFB);

const beforeBoldText = `The phase fraction in the segmented image is ${phaseFrac.toFixed(3)}. Assuming perfect segmentation, the 'ImageRep' model proposed by Dahari et al. suggests that `
const boldText = `we can be ${selectedConf.toFixed(1)}% confident that the material's phase fraction is within ${perErrFromPFB?.toFixed(1)}% of this value (i.e. ${phaseFrac.toFixed(roundTo)}±${(absErrFromPFB).toFixed(roundTo)})`
const boldText = `we can be ${selectedConf.toFixed(1)}% confident that the material's phase fraction is within ${perErrFromPFB?.toFixed(1)}% of this value (i.e. ${phaseFrac.toFixed(3)}±${absErrFromPFB.toFixed(3)})`
const copyText = beforeBoldText + boldText
const afterText = "These results are derived from an estimated "
const afterBoldText = `Characteristic Length Scale (CLS) of ${analysisInfo?.integralRange!.toFixed(0)}px`
Expand Down Expand Up @@ -376,7 +387,7 @@ const Result = () => {
<ListGroup>
<ListGroup.Item variant="dark" style={{ cursor: "pointer" }} onClick={e => handleShowFull()}>Click for Brief explanation!</ListGroup.Item>
<ListGroup.Item>Implementation in the <a href="https://github.com/tldr-group/Representativity">GitHub</a></ListGroup.Item>
<ListGroup.Item>Full details can be found in the <a href="comingsoon">paper</a></ListGroup.Item>
<ListGroup.Item>Full details can be found in the <a href="https://arxiv.org/abs/2410.19568v1">paper</a></ListGroup.Item>
</ListGroup>
</Accordion.Body>
</Accordion.Item>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Popups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export const MoreInfo = () => {
the measured phase fraction with c% of the time.</b>
</p>

<p>Full details can be found in the <a href="comingsoon">paper</a>.</p>
<p>Full details can be found in the <a href="https://arxiv.org/abs/2410.19568v1">paper</a>.</p>
<p>Source code is available <a href="https://github.com/tldr-group/ImageRep">here</a>; please request any features or report any bugs in <a href="https://github.com/tldr-group/ImageRep/issues">the issues page!</a></p>
</Modal.Body>
<Modal.Footer>
<Button variant="dark" onClick={handleClose}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/imageLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export const replaceGreyscaleWithColours = (arr: Uint8ClampedArray, mapping: { [
}

export const getPhaseFraction = (arr: Uint8ClampedArray, val: number, nChannels: number = 4) => {
console.log('ahhhh')
const uniqueVals = arr.filter((_, i, __) => { return i % nChannels == 0 })
const matching = uniqueVals.filter((v) => v == val);
if (arr.length == 0) {return 0}
return (matching.length) / (arr.length / nChannels);
}

Expand Down
57 changes: 29 additions & 28 deletions paper_figures/introduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
sofc_small_im = sofc_large_im[y1:y2, x1:x2]
ax_sofc_im = fig.add_subplot(gs[0, 0])
ax_sofc_im.imshow(sofc_large_im, cmap='gray', interpolation='nearest')
ax_sofc_im.set_xlabel(f"Unknown material's phase fraction: {sofc_large_im.mean():.3f}")
ax_sofc_im.set_xlabel(f"Material's unknown phase fraction: {sofc_large_im.mean():.3f}", fontsize=11)

patch_positions = []
phase_fractions = []
Expand All @@ -74,7 +74,7 @@
# Create the inset:
inset_shift = 1 + wspace
ax_inset = ax_sofc_im.inset_axes([inset_shift, 0, 1, 1], xlim=(x1, x2), ylim=(y1, y2))
ax_inset.set_xlabel(f"Sample's phase fraction: {sofc_small_im.mean():.3f}")
ax_inset.set_xlabel(f"Sample's phase fraction: {sofc_small_im.mean():.3f}", fontsize=11)
inset_pos = ax_inset.get_position()
ax_inset.imshow(sofc_small_im, cmap='gray', interpolation='nearest', extent=[x1, x2, y1, y2])
for spine in ax_inset.spines.values():
Expand All @@ -86,8 +86,8 @@
ax_sofc_im.set_yticks([])
ax_inset.set_xticks([])
ax_inset.set_yticks([])
ax_sofc_im.set_title("(a)")
ax_inset.set_title("(b)")
ax_sofc_im.set_title("(a)", fontsize=12)
ax_inset.set_title("(b)", fontsize=12)



Expand All @@ -99,37 +99,38 @@
x = np.linspace(sofc_large_im.mean() - gap_pfs, sofc_large_im.mean() + gap_pfs, 200)
p = norm.pdf(x, mu, sigma)
unknown_dist.plot(x, p, color=COLOR_IN, linewidth=2,
label="Unknown phase fraction\ndistribution of same-sized samples.")
label="Same-sized samples unknown\nphase fraction distribution.")
# Make the plot 1.5 times lower in the plot:
unknown_dist.set_ylim(0, unknown_dist.get_ylim()[1]*1.6)
unknown_dist.set_ylim(0, unknown_dist.get_ylim()[1]*1.8)
unknown_dist.set_xlim(sofc_large_im.mean() - gap_pfs, sofc_large_im.mean() + gap_pfs)
unknown_dist.set_yticks([])

# Now add the unknown material's phase fraction, until the normal distribution:
ymax = norm.pdf(sofc_large_im.mean(), mu, sigma)
ymax_mean = ymax / unknown_dist.get_ylim()[1]
unknown_dist.axvline(sofc_large_im.mean(), ymax=ymax_mean, color='black', linestyle='--', linewidth=2,
label="Unknown material's phase fraction")
# And the sample phase fraction:
ymax_sample = norm.pdf(sofc_small_im.mean(), mu, sigma)
ymax_sample = ymax_sample / unknown_dist.get_ylim()[1]
ymax_sample = ymax_sample / unknown_dist.get_ylim()[1]
unknown_dist.axvline(sofc_small_im.mean(), ymax=ymax_sample, color=COLOR_INSET, linestyle='--', linewidth=2,
label="Sample's phase fraction")
unknown_dist.legend(loc='upper left')
unknown_dist.set_title("(c)")
unknown_dist.set_xlabel('Phase fraction')
unknown_dist.axvline(sofc_large_im.mean(), ymax=ymax_mean, color='black', linestyle='--', linewidth=2,
label="Material's unknown phase\nfraction")

unknown_dist.legend(loc='upper left', fontsize=11)
unknown_dist.set_title("(c)", fontsize=12)
unknown_dist.set_xlabel('Phase fraction', fontsize=11)


# Rep. calculation:
same_small_im = fig.add_subplot(gs[1, 0])
same_small_im.imshow(sofc_small_im, cmap='gray', interpolation='nearest')
same_small_im.set_xticks([])
same_small_im.set_yticks([])
same_small_im.set_xlabel(f"Single image input")
same_small_im.set_xlabel(f"Single image input", fontsize=11)
for spine in same_small_im.spines.values():
spine.set_edgecolor(COLOR_INSET)
spine.set_linewidth(LINE_W)
same_small_im.set_title('(e)')
same_small_im.set_title('(e)', fontsize=12)

# Create the TPC plot:
tpc_plot = fig.add_subplot(gs[1, 1])
Expand All @@ -138,12 +139,12 @@
tpc_plot.set_ylabel('')
tpc_plot.set_xticks([])
tpc_plot.set_yticks([])
tpc_plot.set_title("(f)")
tpc_plot.set_title("(f)", fontsize=12)

# Text of representativity analysis:
rep_text = fig.add_subplot(gs[1, 2])

microlib_examples = tifffile.imread("paper_figures/microlib_examples.tif")
microlib_examples = tifffile.imread("paper_figures/figure_data/microlib_examples.tif")
microlib_examples_size = min(microlib_examples.shape)
microlib_examples = microlib_examples[-microlib_examples_size:, -microlib_examples_size:]
microlib_examples = microlib_examples / microlib_examples.max()
Expand All @@ -153,13 +154,13 @@
large_microlib_im[-microlib_examples_size:, start_idx:start_idx+microlib_examples_size] = microlib_examples
rep_text.imshow(large_microlib_im, cmap='gray', interpolation='nearest')

rep_text.set_title("(g)")
rep_text.set_title("(g)", fontsize=12)
text_y = 0.1
text_pos = (0.5*large_microlib_im.shape[1], text_y*large_microlib_im.shape[0])
rep_text.text(*text_pos,
"TPC integration and data-driven \ncorrection using MicroLib ", va='center', ha='center')
font_size = rep_text.texts[0].get_fontsize()
rep_text.texts[0].set_fontsize(font_size + 2)
rep_text.texts[0].set_fontsize(font_size + 3)
# delete the axis:
rep_text.axis('off')

Expand All @@ -174,10 +175,10 @@
conf_bounds, pf_1d, cum_sum_sum = get_prediction_interval_stats(sofc_small_im)
# cumulative sum to the original data:
original_data = np.diff(cum_sum_sum)
res_1_pred.plot(pf_1d[1:], original_data, label="Predicted likelihood of material's\nphase fraction", linewidth=2)
res_1_pred.set_ylim(0, res_1_pred.get_ylim()[1]*1.6)
res_1_pred.plot(pf_1d[1:], original_data, label="Predicted likelihood of\nmaterial's phase fraction", linewidth=2)
res_1_pred.set_ylim(0, res_1_pred.get_ylim()[1]*1.8)
res_1_pred.set_xlim(pf_1d[0], pf_1d[-1])
res_1_pred.set_xlabel('Phase fraction')
res_1_pred.set_xlabel('Phase fraction', fontsize=11)
# No y-ticks:
# res_1_pred.set_yticks([])
# Fill between confidence bounds:
Expand Down Expand Up @@ -212,19 +213,19 @@
linestyle="--",
linewidth=2,
color='black',
label="Unknown material's phase fraction",
label="Material's unknown phase\nfraction",
)
# No y-ticks:
res_1_pred.set_yticks([])
res_1_pred.set_title("(d)")
res_1_pred.set_title("(d)", fontsize=12)

res_1_pred.set_ylim([0, res_1_pred.get_ylim()[1]])
inset_pf = sofc_small_im.mean()
xerr = inset_pf - conf_start
res_1_pred.errorbar(
sofc_small_im.mean(), 0.0003, xerr=xerr, fmt='o', capsize=6,
color=COLOR_INSET, label="95% confidence interval", linewidth=LINE_W, capthick=LINE_W)
res_1_pred.legend(loc='upper left')
res_1_pred.legend(loc='upper left', fontsize=11)



Expand Down Expand Up @@ -267,7 +268,7 @@
res_2_pred.set_xticks([])
res_2_pred.set_yticks([])

label = f"Predicted sample size for a smaller\n95% confidence interval, with at\nmost {target_percetange_error}% deviation from material's\nphase fraction instead of {np.round(percent_error*100, 2)}%."
label = f"Sample size needed for ±{target_percetange_error}%\ndeviation from Material's phase\nfraction (95% CI), vs. current\n±{np.round(percent_error*100, 2)}% deviation (± 0.022)."
# Create custom Line2D objects for the spines
spine_line = mlines.Line2D([], [], color='black', linestyle='--',
linewidth=2, alpha=alpha,
Expand All @@ -287,14 +288,14 @@
linewidth=1)

# Add the custom lines to the legend
res_2_pred.legend(handles=[spine_line], loc='upper left')
res_2_pred.legend(handles=[spine_line], loc='upper left', fontsize=11)

for spine in res_2_pred.spines.values():
spine.set_linestyle("--")
spine.set_linewidth(2)
spine.set_alpha(alpha)

res_2_pred.set_title("(h)")
res_2_pred.set_title("(h)", fontsize=12)

positions = []
for i in range(8):
Expand Down Expand Up @@ -356,7 +357,7 @@
# Insert the title as text in the top middle of the rectangle:
text_pos = (lower_left_corner[0] + width_rect/2, lower_left_corner[1] + height_rect - gap_up_down/5)
fig.text(text_pos[0], text_pos[1], title, ha='center', va='center',
fontsize=font_size+4, transform=fig.transFigure)
fontsize=18, transform=fig.transFigure)

fig.patches.append(rect)

Expand Down
3 changes: 2 additions & 1 deletion paper_figures/model_accuracy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
LINE_W = 1.5


def get_prediction_interval_stats(inset_image):
def get_prediction_interval_stats(inset_image, conf=0.95):
phase_fraction = float(np.mean(inset_image))
n_dims = len(inset_image.shape) # 2D or 3D
n_elems = int(np.prod(inset_image.shape))
Expand Down Expand Up @@ -42,6 +42,7 @@ def get_prediction_interval_stats(inset_image):
inset_image.mean(),
std_bern,
std_model,
conf_level=conf,
)

return conf_bounds, pf_1d, cum_sum_sum
Expand Down
Loading