File size: 173,955 Bytes
d66ac34
91a7a11
d66ac34
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
55fc7ca
 
 
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
91a7a11
 
 
 
 
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
 
 
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
55fc7ca
 
 
 
 
91a7a11
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
 
 
 
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
 
 
 
 
 
 
91a7a11
 
 
 
 
 
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
55fc7ca
 
 
 
b7ebbb5
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
91a7a11
 
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
 
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
 
91a7a11
55fc7ca
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
 
 
 
 
 
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
91a7a11
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
91a7a11
 
 
 
 
 
55fc7ca
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
91a7a11
 
55fc7ca
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
91a7a11
 
55fc7ca
 
 
 
91a7a11
 
55fc7ca
 
 
91a7a11
55fc7ca
 
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
91a7a11
 
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
 
 
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
91a7a11
 
 
55fc7ca
 
 
91a7a11
 
 
 
 
55fc7ca
 
68c612b
 
 
91a7a11
68c612b
 
 
91a7a11
68c612b
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
a0ecaf9
91a7a11
55fc7ca
91a7a11
 
a0ecaf9
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
a0ecaf9
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
68c612b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
6409e2f
55fc7ca
 
 
6409e2f
 
 
55fc7ca
 
 
 
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
 
 
 
55fc7ca
6409e2f
 
 
55fc7ca
 
 
6409e2f
 
55fc7ca
 
 
6409e2f
 
55fc7ca
6409e2f
 
55fc7ca
 
 
6409e2f
 
55fc7ca
6409e2f
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
68c612b
 
 
 
55fc7ca
68c612b
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
91a7a11
 
2670ad3
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
a3cf50e
91a7a11
 
a3cf50e
91a7a11
 
a3cf50e
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
a3cf50e
91a7a11
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
 
91a7a11
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
6409e2f
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
a0ecaf9
91a7a11
55fc7ca
 
91a7a11
 
 
 
 
 
a0ecaf9
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
 
 
 
 
 
 
a3cf50e
55fc7ca
 
 
 
 
 
 
 
a3cf50e
55fc7ca
a3cf50e
55fc7ca
a3cf50e
55fc7ca
a3cf50e
 
55fc7ca
a3cf50e
 
 
55fc7ca
 
a3cf50e
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
a3cf50e
91a7a11
 
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
 
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
91a7a11
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
91a7a11
 
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
a3cf50e
55fc7ca
 
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
a3cf50e
55fc7ca
 
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
6409e2f
 
55fc7ca
 
6409e2f
 
 
 
 
a3cf50e
6409e2f
a3cf50e
6409e2f
a3cf50e
55fc7ca
 
a0ecaf9
55fc7ca
a0ecaf9
91a7a11
55fc7ca
a0ecaf9
 
55fc7ca
 
a0ecaf9
a3cf50e
91a7a11
55fc7ca
a0ecaf9
91a7a11
55fc7ca
91a7a11
a0ecaf9
 
55fc7ca
91a7a11
a0ecaf9
 
 
55fc7ca
a0ecaf9
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
a0ecaf9
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
a0ecaf9
55fc7ca
 
91a7a11
a0ecaf9
55fc7ca
 
a0ecaf9
 
 
 
 
 
 
a3cf50e
a0ecaf9
 
55fc7ca
 
6409e2f
 
55fc7ca
 
6409e2f
 
 
 
 
a3cf50e
6409e2f
a3cf50e
6409e2f
a3cf50e
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
55fc7ca
 
91a7a11
55fc7ca
 
91a7a11
 
 
55fc7ca
 
 
91a7a11
 
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
 
55fc7ca
 
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
6409e2f
 
55fc7ca
 
 
 
 
 
 
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
a3cf50e
 
55fc7ca
6409e2f
55fc7ca
 
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
55fc7ca
 
6409e2f
 
55fc7ca
6409e2f
55fc7ca
a3cf50e
55fc7ca
 
 
6409e2f
a3cf50e
6409e2f
55fc7ca
 
 
 
6409e2f
55fc7ca
6409e2f
55fc7ca
6409e2f
 
55fc7ca
6409e2f
a3cf50e
6409e2f
 
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
55fc7ca
 
 
 
6409e2f
55fc7ca
6409e2f
55fc7ca
6409e2f
55fc7ca
 
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
 
 
6409e2f
55fc7ca
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
 
 
 
6409e2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
6409e2f
 
 
 
 
 
55fc7ca
 
 
6409e2f
55fc7ca
 
 
 
 
 
 
 
6409e2f
 
55fc7ca
6409e2f
55fc7ca
49abe21
55fc7ca
 
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
 
55fc7ca
6409e2f
55fc7ca
 
6409e2f
55fc7ca
6409e2f
 
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
 
 
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
 
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
6409e2f
 
 
 
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
55fc7ca
 
6409e2f
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
55fc7ca
 
6409e2f
 
 
 
 
55fc7ca
6409e2f
 
 
 
55fc7ca
 
6409e2f
 
 
55fc7ca
6409e2f
55fc7ca
6409e2f
 
 
 
 
55fc7ca
6409e2f
 
55fc7ca
 
6409e2f
 
 
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
55fc7ca
 
 
6409e2f
55fc7ca
 
 
6409e2f
55fc7ca
6409e2f
 
55fc7ca
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
55fc7ca
 
 
6409e2f
55fc7ca
 
6409e2f
 
55fc7ca
6409e2f
 
 
 
 
 
55fc7ca
6409e2f
 
55fc7ca
 
 
 
 
 
 
 
 
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
 
 
 
 
 
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
 
 
 
 
 
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
 
 
 
 
 
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
 
 
 
 
 
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
 
6409e2f
 
 
 
55fc7ca
6409e2f
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
 
 
 
55fc7ca
6409e2f
 
55fc7ca
 
6409e2f
55fc7ca
 
6409e2f
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
55fc7ca
6409e2f
55fc7ca
 
6409e2f
55fc7ca
 
 
 
6409e2f
4f5b3be
55fc7ca
 
242d43c
 
6409e2f
55fc7ca
6409e2f
 
55fc7ca
6409e2f
 
 
55fc7ca
6409e2f
 
 
 
55fc7ca
6409e2f
 
55fc7ca
 
91a7a11
 
55fc7ca
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
91a7a11
 
 
 
55fc7ca
91a7a11
55fc7ca
 
91a7a11
 
55fc7ca
91a7a11
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
55fc7ca
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
55fc7ca
 
91a7a11
 
55fc7ca
 
 
91a7a11
 
55fc7ca
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
 
 
 
 
 
 
91a7a11
 
55fc7ca
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
55fc7ca
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
55fc7ca
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
91a7a11
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
55fc7ca
 
91a7a11
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
55fc7ca
91a7a11
 
 
 
55fc7ca
 
91a7a11
55fc7ca
 
 
91a7a11
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
91a7a11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
68c612b
91a7a11
55fc7ca
 
 
68c612b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
55fc7ca
 
 
68c612b
55fc7ca
91a7a11
68c612b
 
 
 
 
 
91a7a11
68c612b
 
 
91a7a11
68c612b
 
a3cf50e
55fc7ca
 
 
 
 
 
 
 
a3cf50e
55fc7ca
 
a3cf50e
55fc7ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a7a11
7cfcf32
68c612b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
# ===================================
# 🔁 PART 1: Silent Package Install and Library Imports
# ==================================

import subprocess, sys  # Used for running pip commands and accessing the Python interpreter

# Function to install all required Python packages silently
def install_packages_p1():
    # List of packages required for segmentation, image processing, UI, and Supabase DB access
    packages_p1 = [
        "segmentation_models_pytorch",     # Pre-trained segmentation architectures using PyTorch
        "opencv-python-headless",          # OpenCV without GUI (headless, suitable for servers)
        "matplotlib",                      # For plotting graphs or visualizations (debugging, dev)
        "numpy",                           # Numerical array computations
        "torch",                           # PyTorch for deep learning model operations
        "torchvision",                     # Common datasets/models/transforms for vision
        "albumentations",                  # Fast image augmentations library
        "gradio",                          # Web UI library for model demos/apps
        "supabase"                         # Supabase Python client for auth/database
    ]
    
    # Run pip install silently (no stdout/stderr) for the given packages
    subprocess.run(
        [sys.executable, "-m", "pip", "install", *packages_p1],  # Construct pip install command
        stdout=subprocess.DEVNULL,  # Suppress standard output
        stderr=subprocess.DEVNULL   # Suppress error output
    )

# Call the package installation function
install_packages_p1()

# Print success message after installing packages (visible to the user)
print("✅ Downloaded all necessary libraries successfully (silent mode)")

# ================================
# 📦 Import Required Python Libraries
# ================================

# Standard and third-party libraries used across the app

import os                            # File and directory operations
import cv2                           # OpenCV for image processing
import numpy as np                   # Numerical computations and array handling
import torch                         # PyTorch for ML models
import urllib.request                # To download remote resources/files if needed

import albumentations as A           # Image augmentations
from albumentations.pytorch import ToTensorV2  # Converts numpy arrays to PyTorch tensors

import segmentation_models_pytorch as smp  # Deep segmentation architectures

from matplotlib import pyplot as plt        # Optional: for visual debugging or plotting

from PIL import Image as PILImage_p3        # Pillow for image manipulation (renamed for consistency across parts)

import gradio as gr                         # UI frontend for serving interactive demos

from torch import tensor                    # Explicit import of `tensor` class (optional but handy)
# ================================ 
# 🧠 PART 2: Download and Load All Segmentation Models
# ================================

# 👇 Define GitHub URLs for models (fetched from environment variables)
import os

model_urls_p2 = {
    "toilet_holes": os.getenv("TOILET_HOLES_URL"),  # URL for the toilet holes segmentation model
    "toilet_rim": os.getenv("TOILET_RIM_URL"),      # URL for the toilet rim segmentation model
    "coin_5": os.getenv("COIN_URL")                 # URL for the 5-rupee coin segmentation model
}

# 👇 Define filenames to save the models locally after download
model_paths_p2 = {
    "toilet_holes": "toilet_holes_segmentation_model.pth",  # Local filename for toilet holes model
    "toilet_rim": "toilet_rim_segmentation_model.pth",      # Local filename for toilet rim model
    "coin_5": "5coin_segmentation_model.pth"                # Local filename for coin model
}

# 👇 Function to download model file only if it doesn't already exist locally
def download_model_if_needed_p2(url, filename):
    if not os.path.exists(filename):  # Check if model file is already cached
        print(f"⬇️ Downloading {filename}...")  # Inform user
        urllib.request.urlretrieve(url, filename)  # Download the file from the URL
        print(f"✅ Downloaded: {filename}")  # Confirm success
    else:
        print(f"🟢 Found cached model: {filename}")  # Use cached file to save bandwidth/time

# 👇 Function to download (if needed) and load all three segmentation models
def load_models_p2():
    # Step 1: Ensure all required models are downloaded
    for key in model_paths_p2:
        download_model_if_needed_p2(model_urls_p2[key], model_paths_p2[key])

    # Step 2: Choose device (GPU if available, otherwise CPU)
    device = 'cuda' if torch.cuda.is_available() else 'cpu'

    # Step 3: Load Toilet Holes Model
    model_holes = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1)
    model_holes.load_state_dict(torch.load(model_paths_p2["toilet_holes"], map_location=device))  # Load weights
    model_holes.to(device).eval()  # Move to device and set to evaluation mode
    print("✅ Loaded: Toilet Holes Identification")  # Confirm success

    # Step 4: Load Toilet Rim Model
    model_rim = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1)
    model_rim.load_state_dict(torch.load(model_paths_p2["toilet_rim"], map_location=device))  # Load weights
    model_rim.to(device).eval()  # Move to device and set to evaluation mode
    print("✅ Loaded: Toilet Rim Identification")  # Confirm success

    # Step 5: Load 5-Rupee Coin Reference Model
    model_coinref = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1)
    model_coinref.load_state_dict(torch.load(model_paths_p2["coin_5"], map_location=device))  # Load weights
    model_coinref.to(device).eval()  # Move to device and set to evaluation mode
    print("✅ Loaded: 5 Coin Identification")  # Confirm success

    # Step 6: Return all models and device as a dictionary
    return {
        "device": device,
        "model_holes": model_holes,
        "model_rim": model_rim,
        "model_coinref": model_coinref
    }

# ================================
# 🚀 Load models once when the script starts
# ================================

print("Loading models...")  # Notify start of loading
global_models_and_device = load_models_p2()  # Load models and store in global variable
print("Models loaded successfully.")  # Notify completion

# ================================
# 📦 Extract models and device for easy global access
# ================================

GLOBAL_DEVICE = global_models_and_device["device"]            # CUDA or CPU
GLOBAL_HOLES = global_models_and_device["model_holes"]        # Toilet holes model
GLOBAL_RIM = global_models_and_device["model_rim"]            # Toilet rim model
GLOBAL_COIN = global_models_and_device["model_coinref"]       # Coin model
models_dict = global_models_and_device                        # Dictionary holding all components

# ================================
# 📸 PART 3: Start new session for each user
# ================================

# ✅ Import required modules
from PIL import Image as PILImage       # PIL for image handling (renamed as PILImage to avoid naming conflict)
import numpy as np                      # For numerical operations (may be used in image manipulation later)
import gradio as gr                     # Gradio for building the web UI
import os                               # For file/folder path handling
import uuid                             # To generate unique session identifiers
from datetime import datetime           # (Optional) could be used for timestamped folders or logs

# ✅ Base directory where all user sessions will be stored
BASE_DIR = "user_uploads"               # All user sessions will go under this root folder
os.makedirs(BASE_DIR, exist_ok=True)    # Create the base folder if it doesn't already exist

# ✅ Function to initialize a new session folder with a unique ID
def init_session():
    session_id = str(uuid.uuid4())[:8]         # Generate a short unique session ID using UUID (8 characters)
    session_path = os.path.join(BASE_DIR, session_id)  # Path for this session's folder
    os.makedirs(session_path, exist_ok=True)   # Create a directory for the session
    return session_id                          # Return the session ID (used to reference the folder in other parts)

# ================================
# 🎯 PART 4: Segmentation & Overlay (Multi-user Safe)
# ================================

# ✅ Import necessary libraries
from torchvision import transforms                   # For image preprocessing
from PIL import Image as PILImage_p4                 # PIL for handling images (renamed to avoid conflict with other parts)
from io import BytesIO                               # To store matplotlib plots as in-memory images

# ✅ Define transformation for input images (to Tensor and Normalize)
transform_p4 = transforms.Compose([
    transforms.ToTensor(),                           # Convert image to tensor format (C x H x W)
    transforms.Normalize([0.5]*3, [0.5]*3)            # Normalize RGB channels to range [-1, 1]
])

# ✅ Function to predict mask for a given PIL image using a PyTorch model
def predict_mask_p4(model, device, image_pil):
    model.eval()
    if image_pil is None:
        raise ValueError("❌ Image missing!")  
    # --- Normalize input ---
    if isinstance(image_pil, str) and os.path.exists(image_pil):
        image_pil = PILImage.open(image_pil)
    elif hasattr(image_pil, "read"):  # file-like object
        image_pil.seek(0)
        image_pil = PILImage.open(image_pil)
    elif isinstance(image_pil, PILImage.Image):
        image_pil = image_pil
    elif isinstance(image_pil, np.ndarray):
        if image_pil.ndim == 2:  # grayscale to RGB
            image_pil = np.stack([image_pil]*3, axis=-1)
        elif image_pil.ndim == 3 and image_pil.shape[2] == 4:  # RGBA
            image_pil = image_pil[:, :, :3]
        image_pil = PILImage.fromarray(image_pil.astype(np.uint8))
    else:
        raise ValueError("❌ Unsupported image input type.")# Input validation
    
    # --- Ensure RGB ---
    if image_pil.mode != 'RGB':
        image_pil = image_pil.convert('RGB')

    image_np = np.array(image_pil)                   # Convert PIL image to NumPy array

    if len(image_np.shape) != 3 or image_np.shape[2] != 3:
        raise ValueError("❌ Must be RGB image")      # Ensure it's a 3-channel RGB image

    original_size = (image_np.shape[1], image_np.shape[0])    # Store original size for resizing back later
    resized = cv2.resize(image_np, (256, 256), interpolation=cv2.INTER_LINEAR)                # Resize to model input size

    tensor = transform_p4(resized).unsqueeze(0).to(device.value)   # Apply transform and add batch dimension

    with torch.no_grad():                                     # Disable gradient computation for inference
        out = model(tensor)                                   # Forward pass
        pred = torch.sigmoid(out).squeeze().cpu().numpy()     # Apply sigmoid + remove batch/channel dims
        mask = (pred > 0.5).astype(np.uint8)                  # Threshold to binary mask

        return cv2.resize(mask, original_size, interpolation=cv2.INTER_NEAREST)  # Resize mask back to original size

# ✅ Function to apply a color overlay to the masked region of the input image
def create_overlay_p4(image_pil, mask, color=(255, 0, 0)):
    image_np = np.array(image_pil).copy()             # Convert image to NumPy array
    overlay = image_np.copy()                         # Duplicate for overlay
    overlay[mask == 1] = color                        # Color only where mask is 1
    return overlay                                    # Return overlaid image

# ✅ Function to segment and overlay results on all 3 views: open_noseat, open_seat, closed
def segment_and_overlay_all_p4(img1, img2, img3, model_holes_p2, model_rim_p2, model_coinref_p2, device_p2):
    
    # Get models and device from input state
    model_coin = model_coinref_p2      # Coin segmentation model
    model_holes = model_holes_p2       # Toilet holes segmentation model
    model_rim = model_rim_p2           # Toilet rim segmentation model
    device = device_p2                 # Inference device (CPU or GPU)

    # Organize images by view name
    image_dict = {
        'open_noseat': img1,           # View 1
        'open_seat': img2,             # View 2
        'closed': img3                 # View 3
    }

    # Initialize mask containers for each class and view
    binary_masks = {'ref': {}, 'holes': {}, 'rim': {}}
    grid = []  # To hold overlays for all images in grid format

    # Loop through all three input views
    for key in ['open_noseat', 'open_seat', 'closed']:
        img = image_dict[key]                                 # Get image for current view

        # Predict each of the 3 masks using respective models
        mask_ref = predict_mask_p4(model_coin, device, img)   # Reference object (coin)
        mask_holes = predict_mask_p4(model_holes, device, img)  # Holes
        mask_rim = predict_mask_p4(model_rim, device, img)     # Rim

        # Store masks in dictionary by type and view
        binary_masks['ref'][key] = mask_ref
        binary_masks['holes'][key] = mask_holes
        binary_masks['rim'][key] = mask_rim

        # Create overlays for each mask on top of original image
        overlay_ref = create_overlay_p4(img, mask_ref, (0, 255, 0))     # Green for coin
        overlay_holes = create_overlay_p4(img, mask_holes, (255, 0, 0)) # Red for holes
        overlay_rim = create_overlay_p4(img, mask_rim, (0, 0, 255))     # Blue for rim

        # Append original + overlays to grid
        grid.append([np.array(img), overlay_ref, overlay_holes, overlay_rim])

    # ✅ Create a 3x4 visualization grid using matplotlib
    fig, axes = plt.subplots(3, 4, figsize=(18, 12))     # 3 rows (views) x 4 columns (original + 3 overlays)
    titles = ["Original", "Ref Coin", "Holes", "Rim"]    # Column titles
    rows = ["No Seat", "With Seat", "Closed"]            # Row titles

    for i in range(3):           # Rows
        for j in range(4):       # Columns
            axes[i][j].imshow(grid[i][j])                # Show image
            axes[i][j].axis('off')                       # Hide axis
            if i == 0:
                axes[i][j].set_title(titles[j])          # Set column title
        axes[i][0].text(-50, 128, rows[i], rotation=90, va='center')  # Label row on left side

    plt.tight_layout()           # Prevent overlaps
    buf = BytesIO()              # Create in-memory buffer
    fig.savefig(buf, format='png')    # Save figure to buffer as PNG
    plt.close(fig)               # Close the plot to free memory
    buf.seek(0)                  # Move to start of buffer

    overlay_grid_image = PILImage_p4.open(buf)  # Open saved plot as PIL image

    return overlay_grid_image, binary_masks, image_dict  # Return image grid, masks, and original image dict

# ================================
# 💰 PART 5: ₹5 Coin Detection & px/cm Ratio (Multi-user Safe)
# ================================

from PIL import Image as PILImage_p5  # For image handling
from io import BytesIO               # For in-memory buffer image storage
import matplotlib.pyplot as plt      # For plotting the result overlays
import numpy as np                   # For numerical operations on images
import cv2                           # OpenCV for image processing
import gradio as gr                  # Gradio for UI elements

# Small buffer to reduce fitted ellipse size (to correct overestimation of edge)
reduce_radius_px_p5 = 0

# Actual diameter of ₹5 coin in centimeters
real_diameter_cm_p5 = 2.3

# Function to detect ₹5 coin in each image, estimate its diameter in pixels,
# and compute pixel-per-cm ratio for accurate real-world measurements
def detect_and_plot_reference_p5(image_dict, mask_dict):
    # Dictionary to store calculated px/cm ratios per image
    ref_ratios = {}

    # Set up a 1-row, 3-column matplotlib plot to show results for each image
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    # Process all three images: open without seat, open with seat, closed
    for i, key in enumerate(['open_noseat', 'open_seat', 'closed']):
        # Convert PIL image to NumPy array
        image = np.array(image_dict[key]).copy()

        # Retrieve the binary mask for the ₹5 coin
        mask = mask_dict['ref'][key]

        # Convert binary mask to 8-bit format for contour finding
        mask_u8 = (mask * 255).astype(np.uint8)

        # Find contours in the mask (external only)
        contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # If any contour is detected
        if contours:
            # Choose the largest contour by area (assumed to be the ₹5 coin)
            largest = max(contours, key=cv2.contourArea)

            # Only fit ellipse if the contour has enough points (at least 5)
            if len(largest) >= 5:
                ellipse = cv2.fitEllipse(largest)
                (cx, cy), (major_axis, minor_axis), angle = ellipse

                # Reduce both axes slightly to avoid overestimation
                major_axis = max(major_axis - 2 * reduce_radius_px_p5, 1)
                minor_axis = max(minor_axis - 2 * reduce_radius_px_p5, 1)

                # Average the two axes to estimate the diameter
                avg_diameter_px = (major_axis + minor_axis) / 2

                # Calculate pixel per centimeter ratio
                px_per_cm = avg_diameter_px / real_diameter_cm_p5

                # Save the computed ratio
                ref_ratios[key] = px_per_cm
            else:
                # If ellipse fitting isn't possible, mark as None
                ref_ratios[key] = None

            # Draw a green circle around the coin using min enclosing circle
            (x, y), radius = cv2.minEnclosingCircle(largest)
            radius = max(radius - reduce_radius_px_p5, 0)
            cv2.circle(image, (int(x) - 1, int(y) - 2), int(radius), (0, 255, 0), 2)
        else:
            # If no contour found, set ratio as None
            ref_ratios[key] = None

        # Show result image in the subplot
        axes[i].imshow(image)
        axes[i].axis('off')

        # Set title with calculated px/cm ratio or indicate failure
        axes[i].set_title(f"{key}\n{ref_ratios[key]:.2f} px/cm" if ref_ratios[key] else f"{key}\nNot Detected")

    # Store the matplotlib figure into a PNG image buffer
    buf = BytesIO()
    plt.tight_layout()
    fig.savefig(buf, format='png')
    plt.close(fig)  # Close plot to free memory
    buf.seek(0)

    # Convert buffer image to PIL Image for display in Gradio
    result_img = PILImage_p5.open(buf)

    # Construct a reference text summary for display in textbox
    ref_str = (
        f"Open (No Seat):\n{ref_ratios['open_noseat']:4.2f}px/cm\n"
        f"\nOpen (With Seat):\n{ref_ratios['open_seat']:4.2f}px/cm\n"
        f"\nClosed:\n{ref_ratios['closed']:4.2f}px/cm"
    )

    # Return:
    # - Updated image with overlays,
    # - px/cm ratios for each image,
    # - Text summary of ratios,
    # - Indicator to show text output component
    return gr.update(value=result_img, visible=True), ref_ratios, gr.update(value=ref_str, visible=True), gr.update(visible=True)

# ================================
# 📐 PART 6: Rim Measurement + Visualization (Multi-user Safe)
# ================================

from PIL import Image as PILImage_p6               # For working with final PIL image output
from io import BytesIO                             # To handle in-memory image buffer
import math                                        # For trigonometric calculations
import numpy as np                                 # For numerical operations and arrays
import cv2                                         # OpenCV for image processing
import matplotlib.pyplot as plt                    # For plotting annotated visuals
import gradio as gr                                # Gradio for UI components

# Function to analyze inner and outer rim from the mask using ellipse and directional probing
def analyze_rim_intersections_p6(image_dict, mask_dict, ref_ratios):
    image_key = 'open_seat'                        # Only operate on open seat image
    mask = mask_dict['rim'][image_key]             # Get rim mask for selected image
    mask_u8 = (mask * 255).astype(np.uint8)        # Convert binary mask to 8-bit for OpenCV

    # Find all contours in the rim mask
    contours, _ = cv2.findContours(mask_u8, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    if len(contours) < 2:
        raise ValueError("❌ Need both inner and outer contours.")  # Need both rim edges

    # Sort by area: outer will be largest, inner next
    outer, inner = sorted(contours, key=cv2.contourArea, reverse=True)[:2]

    # Ensure inner contour has enough points to fit an ellipse
    if len(inner) < 5:
        raise ValueError("❌ Inner contour too small for ellipse fitting.")
    
    ellipse = cv2.fitEllipse(inner)
    (xc, yc), (MA, ma), angle = ellipse  # Center, axes, and angle from ellipse fit

    # Swap angle to be vertical major axis if needed
    if MA < ma:
        angle += 90

    # Convert angle to radians and compute unit direction vector pointing 'down'
    angle_rad = np.deg2rad(angle)
    dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)])
    if dir_down[1] < 0:  # Ensure it points downward in image
        dir_down *= -1

    # Get orthogonal 'right' direction by rotating 90 degrees clockwise
    dir_right = np.array([-dir_down[1], dir_down[0]])
    if dir_right[0] < 0:  # Ensure it points rightward
        dir_right *= -1

    # Compute bounding points and center from inner contour
    topmost = tuple(inner[inner[:, :, 1].argmin()][0])
    bottommost = tuple(inner[inner[:, :, 1].argmax()][0])
    center_y = (topmost[1] + bottommost[1]) // 2

    leftmost = tuple(inner[inner[:, :, 0].argmin()][0])
    rightmost = tuple(inner[inner[:, :, 0].argmax()][0])
    center_x = (leftmost[0] + rightmost[0]) // 2

    center = np.array([center_x, center_y])  # Final center point of rim

    # Function to find intersection of a ray in a direction with the mask
    def get_intersections(mask, center, direction, max_steps=7000):
        prev = mask[int(center[1]), int(center[0])]
        outer_pt, inner_pt = None, None
        last_valid = None

        for step in range(1, max_steps):
            pt = center + step * direction
            x, y = int(round(pt[0])), int(round(pt[1]))
            if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]):
                break  # Stop if point goes out of bounds
            val = mask[y, x]
            last_valid = np.array([x, y])

            # Detect transition from background to mask (outer edge)
            if outer_pt is None and val == 1 and prev == 0:
                outer_pt = np.array([x, y])
            # Detect transition from mask to background (inner edge)
            elif outer_pt is not None and val == 0 and prev == 1:
                inner_pt = np.array([x, y])
                break
            prev = val

        if inner_pt is None:  # If no edge found, use last seen point
            inner_pt = last_valid
        return outer_pt, inner_pt

    # Probe down and right directions from center to find outer and inner rim points
    inner_down, outer_down = get_intersections(mask, center, dir_down)
    inner_right, outer_right = get_intersections(mask, center, dir_right)

    # Helper function to calculate Euclidean distance
    def dist(a, b): return np.linalg.norm(a - b) if a is not None and b is not None else None

    # Compute distances from center to points (in pixels)
    d_down_outer_px = dist(center, outer_down)
    d_down_inner_px = dist(center, inner_down)
    d_right_inner_px = dist(center, inner_right)
    d_right_outer_px = dist(center, outer_right)

    # Rim width = outer - inner in both directions
    rim_width_down = d_down_outer_px - d_down_inner_px
    rim_width_right = d_right_outer_px - d_right_inner_px

    # Get pixel/cm ratio for conversion
    px_per_cm = ref_ratios[image_key]

    # Conversion lambdas from pixels to cm/inch
    to_cm = lambda px: px / px_per_cm if px is not None else None
    to_in = lambda px: px / px_per_cm / 2.54 if px is not None else None

    # Formatter for readable output
    fmt = lambda val: f"{val:.2f}" if val else "N/A"

    # Dictionary of dimensions in cm
    rim_cm = {
        "down_inner": to_cm(d_down_inner_px),
        "down_outer": to_cm(d_down_outer_px),
        "right_inner": to_cm(d_right_inner_px),
        "right_outer": to_cm(d_right_outer_px),
        "width_down": to_cm(rim_width_down),
        "width_right": to_cm(rim_width_right)
    }

    # Dictionary of dimensions in inches
    rim_inch = {
        "down_inner": to_in(d_down_inner_px),
        "down_outer": to_in(d_down_outer_px),
        "right_inner": to_in(d_right_inner_px),
        "right_outer": to_in(d_right_outer_px),
        "width_down": to_in(rim_width_down),
        "width_right": to_in(rim_width_right)
    }

    # Text labels for Gradio UI
    rim_ui = {
        "Down Inner": f"{fmt(rim_cm['down_inner'])} cm | {fmt(rim_inch['down_inner'])} in",
        "Down Outer": f"{fmt(rim_cm['down_outer'])} cm | {fmt(rim_inch['down_outer'])} in",
        "Right Inner": f"{fmt(rim_cm['right_inner'])} cm | {fmt(rim_inch['right_inner'])} in",
        "Right Outer": f"{fmt(rim_cm['right_outer'])} cm | {fmt(rim_inch['right_outer'])} in",
    }

    # Load the image to draw results on
    pil_img = image_dict[image_key]
    if pil_img is None:
        raise ValueError("❌ No image found for analysis!")
    image_vis = np.array(pil_img)

    # Draw center point
    cv2.circle(image_vis, center, 4, (255, 255, 0), 5)

    # Draw measurement lines and dots
    for pt, color in zip(
        [outer_down, inner_down, outer_right, inner_right],
        [(255, 0, 0), (0, 255, 0), (255, 0, 255), (255, 255, 0)]
    ):
        if pt is not None:
            cv2.line(image_vis, center, pt, color, 2)
            cv2.circle(image_vis, pt, 4, color, 5)

    # Draw directional arrows for orientation
    cv2.arrowedLine(image_vis, center, (center + dir_down * 100).astype(int), (0, 255, 0), 2)
    cv2.arrowedLine(image_vis, center, (center + dir_right * 100).astype(int), (0, 0, 255), 2)

    # Helper to draw label near point
    def draw_label(pt, label, offset):
        if pt is not None:
            pos = pt + offset
            plt.text(pos[0], pos[1], label, fontsize=9, color='white', ha='center', va='center', bbox=dict(facecolor='black', alpha=0.6, boxstyle='round,pad=0.3'))

    # Visualize final annotated image
    plt.figure(figsize=(6, 6))
    plt.imshow(image_vis)
    offset = np.array([0, 70])
    draw_label(inner_down, rim_ui["Down Inner"], -offset)
    draw_label(outer_down, rim_ui["Down Outer"], -offset)
    draw_label(inner_right, rim_ui["Right Inner"], -offset)
    draw_label(outer_right, rim_ui["Right Outer"], offset)
    plt.axis("off")

    # Convert plot to PIL image for Gradio
    buf = BytesIO()
    plt.savefig(buf, format='png')
    plt.close()
    buf.seek(0)
    vis_img = PILImage_p6.open(buf)

    # Build textual summary of all measurements
    seat_str = (
        f"Down Inner Diameter:\n{d_down_inner_px*2:4.1f}px | {rim_cm['down_inner']*2:4.2f}cm | {rim_inch['down_inner']*2:4.2f}in\n"
        f"\nRight Inner Diameter:\n{d_right_inner_px*2:4.1f}px | {rim_cm['right_inner']*2:4.2f}cm | {rim_inch['right_inner']*2:4.2f}in\n"
        f"\nRight Outer Diameter:\n{d_right_outer_px*2:4.1f}px | {rim_cm['right_outer']*2:4.2f}cm | {rim_inch['right_outer']*2:4.2f}in\n"
        f"\nDown Angle:\n{(angle - 90) % 360:4.1f}°\n"
        f"\nRight Angle:\n{angle:4.1f}°\n"
        f"\nWidth Down:\n{rim_width_down:4.1f}px | {rim_cm['width_down']:4.2f}cm | {rim_inch['width_down']:4.2f}in\n"
        f"\nWidth Right:\n{rim_width_right:4.1f}px | {rim_cm['width_right']:4.2f}cm | {rim_inch['width_right']:4.2f}in"
    )

    # Return:
    # - Annotated image update
    # - Rim values for UI
    # - Rim values in cm
    # - Rim values in inches
    # - Full summary text
    # - Show result components
    return gr.update(value=vis_img, visible=True), rim_ui, rim_cm, rim_inch, gr.update(value=seat_str, visible=True), gr.update(visible=True)

# ================================
# 📐 PART 7: Ellipse-Based Rim Width Measurement from Image (open_noseat)
# ================================

from PIL import Image as PILImage_p7  # PIL for image manipulation
from io import BytesIO               # To handle image bytes buffer
import math                          # For trigonometric functions
import numpy as np                  # For array and math operations
import cv2                          # OpenCV for contour/ellipse analysis
import matplotlib.pyplot as plt     # For image plotting and annotation

# function to analyze rim angles and calculate dimensions (down and right diameters)
def analyze_rim_intersections_p7(image_dict, mask_dict, ref_ratios):
    image_key = 'open_noseat'  # Key used to fetch image/mask
    mask = mask_dict['rim'][image_key]  # Get binary mask for rim
    mask_u8 = (mask * 255).astype(np.uint8)  # Convert mask to 8-bit image for OpenCV

    # Find contours in the mask
    contours, _ = cv2.findContours(mask_u8, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    # Ensure we have both inner and outer contours
    if len(contours) < 2:
        raise ValueError("❌ Need both inner and outer contours.")
    
    # Sort contours by area and select the two largest (outer and inner)
    outer, inner = sorted(contours, key=cv2.contourArea, reverse=True)[:2]

    # Ensure inner contour is large enough for ellipse fitting
    if len(inner) < 5:
        raise ValueError("❌ Outer contour too small for ellipse fitting.")
    
    # Fit an ellipse to the inner contour
    ellipse = cv2.fitEllipse(inner)
    (xc, yc), (MA, ma), angle = ellipse  # center, major axis, minor axis, and rotation angle

    # Ensure angle reflects long axis vertically
    if MA < ma:
        angle += 90

    # Convert angle to radians and compute direction vectors
    angle_rad = np.deg2rad(angle)
    dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)])
    if dir_down[1] < 0:
        dir_down *= -1  # Flip to ensure it points down

    # Right direction is perpendicular to down direction
    dir_right = np.array([-dir_down[1], dir_down[0]])
    if dir_right[0] < 0:
        dir_right *= -1  # Flip to ensure rightward direction

    # Compute vertical center between top and bottom points
    topmost = tuple(inner[inner[:, :, 1].argmin()][0])
    bottommost = tuple(inner[inner[:, :, 1].argmax()][0])
    center_y = (topmost[1] + bottommost[1]) // 2

    # Compute horizontal center between left and right points
    leftmost = tuple(inner[inner[:, :, 0].argmin()][0])
    rightmost = tuple(inner[inner[:, :, 0].argmax()][0])
    center_x = (leftmost[0] + rightmost[0]) // 2

    center = np.array([center_x, center_y])  # Geometric center

    # function to get intersections of the line with the mask to get inner and outer points
    def get_intersections(mask, center, direction, max_steps=7000):
        prev = mask[int(center[1]), int(center[0])]  # Start from center
        outer_pt, inner_pt = None, None
        last_valid = None

        for step in range(1, max_steps):
            pt = center + step * direction  # Move along the direction
            x, y = int(round(pt[0])), int(round(pt[1]))

            # Stop if point is outside image bounds
            if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]):
                break

            val = mask[y, x]
            last_valid = np.array([x, y])

            # Detect outer boundary
            if outer_pt is None and val == 1 and prev == 0:
                outer_pt = np.array([x, y])
            # Detect inner boundary after crossing outer
            elif outer_pt is not None and val == 0 and prev == 1:
                inner_pt = np.array([x, y])
                break
            prev = val

        # If no inner found, use last valid point
        if inner_pt is None:
            inner_pt = last_valid
        return outer_pt, inner_pt

    # Get inner/outer points in both down and right directions
    inner_down, outer_down = get_intersections(mask, center, dir_down)
    inner_right, outer_right = get_intersections(mask, center, dir_right)

    # function to calculate distance between two points
    def dist(a, b): return np.linalg.norm(a - b) if a is not None and b is not None else None

    # Compute distances from center to inner and outer rim boundaries
    d_down_outer_px = dist(center, outer_down)
    d_down_inner_px = dist(center, inner_down)
    d_right_inner_px = dist(center, inner_right)
    d_right_outer_px = dist(center, outer_right)

    # Rim thickness = outer - inner distances
    rim_width_down = d_down_outer_px - d_down_inner_px
    rim_width_right = d_right_outer_px - d_right_inner_px

    # Conversion factors: pixels per cm
    px_per_cm = ref_ratios[image_key]
    to_cm = lambda px: px / px_per_cm if px is not None else None
    to_in = lambda px: px / px_per_cm / 2.54 if px is not None else None
    fmt = lambda val: f"{val:.2f}" if val else "N/A"  # formatted output

    # Measurements in cm
    rim_cm = {
        "down_inner": to_cm(d_down_inner_px),
        "down_outer": to_cm(d_down_outer_px),
        "right_inner": to_cm(d_right_inner_px),
        "right_outer": to_cm(d_right_outer_px),
        "width_down": to_cm(rim_width_down),
        "width_right": to_cm(rim_width_right)
    }

    # Measurements in inches
    rim_inch = {
        "down_inner": to_in(d_down_inner_px),
        "down_outer": to_in(d_down_outer_px),
        "right_inner": to_in(d_right_inner_px),
        "right_outer": to_in(d_right_outer_px),
        "width_down": to_in(rim_width_down),
        "width_right": to_in(rim_width_right)
    }

    # String for user interface display
    rim_ui = {
        "Down Inner": f"{fmt(rim_cm['down_inner'])} cm | {fmt(rim_inch['down_inner'])} in",
        "Down Outer": f"{fmt(rim_cm['down_outer'])} cm | {fmt(rim_inch['down_outer'])} in",
        "Right Inner": f"{fmt(rim_cm['right_inner'])} cm | {fmt(rim_inch['right_inner'])} in",
        "Right Outer": f"{fmt(rim_cm['right_outer'])} cm | {fmt(rim_inch['right_outer'])} in",
    }

    # Get original image
    pil_img = image_dict[image_key]
    if pil_img is None:
        raise ValueError("❌ No image found for analysis!")
    image_vis = np.array(pil_img)

    # Draw central point
    cv2.circle(image_vis, center.astype(int), 4, (255, 255, 0), 5)

    # Draw lines and circles for each measurement point
    for pt, color in zip(
        [outer_down, inner_down, outer_right, inner_right],
        [(255, 0, 0), (0, 255, 0), (255, 0, 255), (255, 255, 0)]
    ):
        if pt is not None:
            cv2.line(image_vis, center.astype(int), pt, color, 2)
            cv2.circle(image_vis, pt, 4, color, 5)

    # Draw direction arrows for reference
    cv2.arrowedLine(image_vis, center.astype(int), (center + dir_down * 100).astype(int), (0, 255, 0), 2)
    cv2.arrowedLine(image_vis, center.astype(int), (center + dir_right * 100).astype(int), (0, 0, 255), 2)

    # function to draw labels on the image
    def draw_label(pt, label, offset):
        if pt is not None:
            pos = pt + offset
            plt.text(pos[0], pos[1], label, fontsize=9, color='white', ha='center', va='center',
                     bbox=dict(facecolor='black', alpha=0.6, boxstyle='round,pad=0.3'))

    # Display image with annotations
    plt.figure(figsize=(6, 6))
    plt.imshow(image_vis)
    offset = np.array([0, 70])
    draw_label(inner_down, rim_ui["Down Inner"], -offset)
    draw_label(outer_down, rim_ui["Down Outer"], -offset)
    draw_label(inner_right, rim_ui["Right Inner"], -offset)
    draw_label(outer_right, rim_ui["Right Outer"], offset)
    plt.axis("off")

    # Save visualization to memory buffer
    buf = BytesIO()
    plt.savefig(buf, format='png')
    plt.close()
    buf.seek(0)
    vis_img = PILImage_p7.open(buf)  # Open buffer as image
    
    # Formatted string summary of measurements
    rim_str = (
        f"Down Inner Diameter:\n{d_down_inner_px*2:4.1f}px | {rim_cm['down_inner']*2:4.2f}cm | {rim_inch['down_inner']*2:4.2f}in\n"
        f"\nRight Inner Diameter:\n{d_right_inner_px*2:4.1f}px | {rim_cm['right_inner']*2:4.2f}cm | {rim_inch['right_inner']*2:4.2f}in\n"
        f"\nRight Outer Diameter:\n{d_right_outer_px*2:4.1f}px | {rim_cm['right_outer']*2:4.2f}cm | {rim_inch['right_outer']*2:4.2f}in\n"
        f"\nDown Angle:\n{(angle - 90) % 360:4.1f}°\n"
        f"\nRight Angle:\n{angle:4.1f}°\n"
        f"\nWidth Down:\n{rim_width_down:4.1f}px | {rim_cm['width_down']:4.2f}cm | {rim_inch['width_down']:4.2f}in\n"
        f"\nWidth Right:\n{rim_width_right:4.1f}px | {rim_cm['width_right']:4.2f}cm | {rim_inch['width_right']:4.2f}in\n"
    )

    # Return: image update, UI string, numeric outputs, vector info, visibility updates
    return (
        gr.update(value=vis_img, visible=True),  # Annotated image
        rim_ui,  # Display text (cm/in)
        rim_cm,  # Numerical values in cm
        rim_inch,  # Numerical values in inches
        center.tolist(),  # Center coordinates as list
        dir_down.tolist(),  # Down direction vector
        dir_right.tolist(),  # Right direction vector
        gr.update(value=rim_str, visible=True),  # Measurement summary text
        gr.update(visible=True)  # Toggle visibility flag
    )

# ================================
# 🧩 PART 8: Rim Height Analyzer (Stateless, Session-Safe)
# ================================

import gradio as gr
import numpy as np
import cv2
import math
from PIL import Image as PILImage_p8
from io import BytesIO
import matplotlib.pyplot as plt

# ============================
# Helper function to find the inner top point of the rim (searching in "down" direction)
# ============================
def find_inner_top_p8(mask, center, direction, max_steps=7000):
    prev = mask[int(center[1]), int(center[0])]  # Get initial pixel value at center
    for step in range(1, max_steps):
        pt = center + step * direction  # Step along the direction vector
        x, y = int(round(pt[0])), int(round(pt[1]))
        if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]):  # If outside bounds, stop
            break
        val = mask[y, x]  # Get pixel value at new location
        if prev == 1 and val == 0:  # Detect the transition from rim (1) to background (0)
            return np.array([x, y])  # Return the topmost point of inner rim
        prev = val
    return None  # Return None if not found

# ============================
# Helper function to find the outer bottom point of the rim (searching in "up" direction)
# ============================
def find_outer_bottom_p8(mask, center, direction, max_steps=7000):
    prev = mask[int(center[1]), int(center[0])]  # Get initial pixel value
    for step in range(1, max_steps):
        pt = center - step * direction  # Step in opposite direction
        x, y = int(round(pt[0])), int(round(pt[1]))
        if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]):  # Out of bounds check
            break
        val = mask[y, x]  # Pixel value at new point
        if prev == 0 and val == 1:  # Transition from background (0) to rim (1)
            return np.array([x, y])  # Return bottommost outer rim point
        prev = val
    return None  # Return None if not found

# ============================
# Main analysis function to compute rim height and visualize it
# ============================
def run_rim_height_analysis_p8(
    _trigger_button,       # Dummy input for Gradio button triggering
    image_dict_p4,         # Dictionary of input images (session state)
    binary_masks_p4,       # Dictionary of binary masks (rim)
    ref_ratios_p5,         # Reference pixel/cm conversion ratio
    center_p7,             # Center point for analysis
    dir_down_p7            # Downward vector for analysis
):
    # Load image and relevant mask
    image = np.array(image_dict_p4["open_noseat"]).copy()
    mask = binary_masks_p4["rim"]["open_noseat"]
    px_per_cm = ref_ratios_p5["open_noseat"]

    # Convert center and direction to numpy arrays
    center_pt = np.array(center_p7)
    dir_vec = np.array(dir_down_p7)

    # Compute angles for display
    raw_angle = np.rad2deg(math.atan2(dir_vec[1], dir_vec[0]))
    down_angle_deg = (450 - raw_angle) % 360
    perp_angle_deg = (down_angle_deg + 90) % 360

    # Get the rim top and bottom using mask edge transitions
    inner_top = find_outer_bottom_p8(mask, center_pt, dir_vec)
    outer_bottom = find_inner_top_p8(mask, center_pt, dir_vec)

    # If any point is missing, abort and inform the user
    if inner_top is None or outer_bottom is None:
        return "⚠️ Could not find both rim points", None, None, None, None, None, None, None

    # Calculate pixel distance between points (rim height)
    rim_height_px = np.linalg.norm(outer_bottom - inner_top)
    rim_height_cm = rim_height_px / px_per_cm
    rim_height_in = rim_height_cm / 2.54

    # ------------------------------------
    # Visualization of points and line
    # ------------------------------------
    # Mark points
    cv2.circle(image, tuple(inner_top), 5, (0, 255, 255), -1)  # Yellow inner top
    cv2.circle(image, tuple(outer_bottom), 5, (255, 255, 0), -1)  # Cyan outer bottom
    # Draw connecting line
    cv2.line(image, tuple(inner_top), tuple(outer_bottom), (0, 0, 255), 2)  # Red line

    # Draw direction arrow for debugging
    arrow_end = (center_pt + dir_vec * 100).astype(int)
    cv2.arrowedLine(image, center_pt.astype(int), arrow_end, (0, 255, 0), 2)  # Green arrow

    # Add text overlay with rim height
    label = f"{rim_height_px:.1f}px | {rim_height_cm:.2f}cm | {rim_height_in:.2f}in"
    mid = ((inner_top + outer_bottom) / 2).astype(int)
    text_pos = (mid[0] + 10, mid[1] - 10)

    # Draw background rectangle behind text for readability
    overlay = image.copy()
    font = cv2.FONT_HERSHEY_SIMPLEX
    (tw, th), _ = cv2.getTextSize(label, font, 1, 2)
    rect_start = (text_pos[0] - 10, text_pos[1] - th - 10)
    rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10)
    cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1)  # Black background
    cv2.addWeighted(overlay, 0.5, image, 0.5, 0, image)  # Blend it with image

    # Final text overlay
    cv2.putText(image, label, text_pos, font, 1, (255, 255, 255), 2, cv2.LINE_AA)

    # Convert final OpenCV image to PIL for Gradio
    buf = BytesIO()
    plt.imsave(buf, image)  # Save with matplotlib to buffer
    buf.seek(0)
    rim_result_image_p8 = PILImage_p8.open(buf)

    # Format rim height string for display in output textbox
    rim_str = (
        f"Rim Height:\n{rim_height_px:4.1f}px | {rim_height_cm:4.2f}cm | {rim_height_in:4.2f}in\n\nDown Angle:\n{down_angle_deg:4.1f}°"
    )

    # Return outputs for Gradio UI: label string, annotated image, raw values, and updated States
    return (
        label,  # label string
        gr.update(value=rim_result_image_p8, visible=True),  # image with annotations
        rim_height_px,  # rim height in pixels
        rim_height_cm,  # rim height in centimeters
        rim_height_in,  # rim height in inches
        inner_top.tolist(),  # top point of rim (to pass to later stages)
        gr.update(value=rim_str, visible=True),  # rim result text box
        gr.update(visible=True)  # signal visibility of any dependent outputs
    )

# ============================
# 🕳️ PART 9: Hole Width Measurement (Stateless)
# ============================

from PIL import Image as PILImage_p9  # Import PIL for image handling
from io import BytesIO               # For in-memory image saving/loading

# Function to compute the perpendicular width of a hole (e.g. toilet bowl opening)
def analyze_hole_width_perpendicular_p9(
    _trigger,               # Dummy trigger input to enable button-based execution
    binary_masks_p4,        # Dictionary containing binary segmentation masks
    ref_ratios_p5,          # Reference pixel-per-cm conversion ratios
    image_dict_p4,          # Dictionary of input images by category
    dir_right_p7,           # Direction vector (e.g. right axis from ellipse)
    center_p7               # Ellipse center point (not used here, but may be useful contextually)
):
    # --- Input setup ---
    mask = binary_masks_p4["holes"]["open_noseat"]  # Get binary mask for the hole (open_noseat type)
    mask_u8 = (mask * 255).astype(np.uint8)         # Convert binary mask to uint8 for image ops
    ys, xs = np.where(mask == 1)                    # Get coordinates of all white pixels (non-zero)
    points = np.stack([xs, ys], axis=1).astype(float)  # Stack as Nx2 float array of points

    # --- Compute perpendicular direction ---
    dir_scan = np.array(dir_right_p7)              # Use provided right direction vector
    dir_scan = dir_scan / np.linalg.norm(dir_scan) # Normalize to unit vector
    dir_perp = np.array([-dir_scan[1], dir_scan[0]])  # Compute perpendicular direction

    # --- Project points on perpendicular axis ---
    projs = points @ dir_perp                      # Project each point on perpendicular axis
    proj_min, proj_max = np.min(projs), np.max(projs)  # Get min and max projection values

    # --- Find widest span along perpendicular lines ---
    best_dist = -1
    pt_min_p9 = None
    pt_max_p9 = None
    for offset in np.arange(proj_min, proj_max, 1.0):  # Slide line across projection axis
        mask_line = np.abs(projs - offset) < 0.5       # Select nearby points close to this offset
        line_points = points[mask_line]                # Get actual coordinates for those points
        if len(line_points) >= 2:                      # Only consider if line has ≥ 2 points
            line_proj = line_points @ dir_scan         # Project onto scanning direction
            i_min = np.argmin(line_proj)               # Get point with min projection
            i_max = np.argmax(line_proj)               # Get point with max projection
            d = np.linalg.norm(line_points[i_max] - line_points[i_min])  # Compute distance
            if d > best_dist:                          # If it's the longest so far, save it
                best_dist = d
                pt_min_p9 = line_points[i_min]
                pt_max_p9 = line_points[i_max]

    # --- Handle edge case where no line pair found ---
    if pt_min_p9 is None or pt_max_p9 is None:
        return None, None, None, None, None, None, None

    # --- Compute pixel width ---
    hole_width_px_p9 = best_dist  # Best span found is hole width in pixels

    # --- Compute orientation angle of detected line ---
    dir_line = pt_max_p9 - pt_min_p9
    dir_line = dir_line / np.linalg.norm(dir_line)  # Normalize
    angle_rad = np.arctan2(dir_line[1], dir_line[0])  # Angle in radians
    angle_deg_p9 = (450 - np.rad2deg(angle_rad)) % 360  # Convert to degrees (clockwise from up)

    # --- Convert width from pixels to cm/inch ---
    px_per_cm = ref_ratios_p5["open_noseat"]     # Get px/cm ratio
    cm_per_px = 1.0 / px_per_cm                  # Inverse gives cm/px
    hole_width_cm_p9 = hole_width_px_p9 * cm_per_px  # Convert to cm
    hole_width_inch_p9 = hole_width_cm_p9 / 2.54      # Convert to inches

    # --- Visualization on image ---
    image = np.array(image_dict_p4["open_noseat"]).copy()  # Copy image for annotation
    cv2.circle(image, pt_min_p9.astype(int), 5, (0, 255, 0), -1)  # Draw green circle at pt1
    cv2.circle(image, pt_max_p9.astype(int), 5, (0, 0, 255), -1)  # Draw red circle at pt2
    cv2.line(image, pt_min_p9.astype(int), pt_max_p9.astype(int), (255, 255, 0), 2)  # Yellow line

    # --- Prepare label text ---
    label = f"{hole_width_px_p9:.1f}px | {hole_width_cm_p9:.2f}cm | {hole_width_inch_p9:.2f}in"
    mid = ((pt_min_p9 + pt_max_p9) / 2).astype(int)  # Midpoint between two points
    text_pos = (mid[0] - 30, mid[1] - 30)            # Text offset for visibility

    overlay = image.copy()                           # For semi-transparent text box
    font = cv2.FONT_HERSHEY_SIMPLEX
    (tw, th), _ = cv2.getTextSize(label, font, 1, 2)  # Get text width/height
    rect_start = (text_pos[0] - 10, text_pos[1] - th - 10)  # Top-left of background rectangle
    rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10)    # Bottom-right of rectangle
    cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1)  # Draw black box behind text
    cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image)          # Blend with original
    cv2.putText(image, label, text_pos, font, 1, (255, 255, 255), 2)  # Final text on image

    # --- Save result image to in-memory buffer ---
    buf = BytesIO()
    plt.imsave(buf, image)           # Save annotated image to buffer
    buf.seek(0)
    hole_result_image_p9 = PILImage_p9.open(buf)  # Load back as PIL image
    
    # --- Prepare measurement string for display ---
    hole_width_str = (
        f"Hole Width:\n{hole_width_px_p9:4.1f}px | {hole_width_cm_p9:4.2f}cm | {hole_width_inch_p9:4.2f}in\n"
        f"\nOrientation Angle:\n{angle_deg_p9:4.1f}°"
    )

    # --- Return multiple outputs for Gradio UI ---
    return (
        gr.update(value=hole_result_image_p9, visible=True),  # Annotated image output
        hole_width_px_p9,                                     # Raw width in pixels
        hole_width_cm_p9,                                     # Width in cm
        hole_width_inch_p9,                                   # Width in inches
        angle_deg_p9,                                         # Orientation angle
        pt_min_p9.tolist(),                                   # Point 1 coords
        pt_max_p9.tolist(),                                   # Point 2 coords
        gr.update(value=hole_width_str, visible=True),        # Text summary
        gr.update(visible=True)                               # Trigger visibility for some UI element
    )

# =============================
# 🧩 PART 10: Rim-to-Hole Line Distance (Stateless)
# =============================

from PIL import Image as PILImage_p10
from io import BytesIO

def compute_top_to_hole_distance_p10(
    _trigger,            # dummy trigger to force execution in Gradio pipeline
    inner_top_p8,        # topmost point on the inner rim (from Part 8)
    pt_min_p9,           # one end of the horizontal hole line (from Part 9)
    pt_max_p9,           # other end of the horizontal hole line (from Part 9)
    dir_down_p7,         # direction vector pointing downward from rim (from Part 7)
    ref_ratios_p5,       # px/cm ratios per view (from Part 5)
    image_dict_p4        # original rotated images (from Part 4)
):
    # Helper function to find intersection point between two parametric lines
    def line_intersection_p10(p1, d1, p2, d2):
        A = np.array([d1, -d2]).T      # construct matrix from direction vectors
        b = p2 - p1                    # vector between starting points
        if np.linalg.matrix_rank(A) < 2:
            return None               # lines are parallel; no intersection
        t_s = np.linalg.lstsq(A, b, rcond=None)[0]  # solve A*[t, s] = b
        return p1 + t_s[0] * d1        # return intersection point along line 1

    # Convert input points and vectors to numpy arrays for math operations
    pt_min = np.array(pt_min_p9)
    pt_max = np.array(pt_max_p9)
    inner_top = np.array(inner_top_p8)
    dir_down = np.array(dir_down_p7)

    # Compute the direction vector of the hole line (horizontal across hole)
    dir_hole_line = pt_max - pt_min
    dir_hole_line = dir_hole_line / np.linalg.norm(dir_hole_line)  # normalize

    # Compute intersection point between inner rim line and hole line
    intersection_point = line_intersection_p10(inner_top, dir_down, pt_min, dir_hole_line)

    if intersection_point is None:
        print("❌ Lines are parallel.")  # alert if intersection fails
        return None, None, None, None, None, None, None

    # --- Distance Calculation ---
    dist_px = np.linalg.norm(intersection_point - inner_top)  # pixel distance
    px_per_cm = ref_ratios_p5["open_noseat"]                  # px/cm for current view
    dist_cm = dist_px / px_per_cm                             # convert to cm
    dist_inch = dist_cm / 2.54                                # convert to inches

    # --- Angle Calculations ---
    # Convert angle of rim direction to degrees (clockwise from top)
    angle_down_deg = (450 - np.rad2deg(np.arctan2(dir_down[1], dir_down[0]))) % 360
    # Convert angle of hole line direction to degrees
    vec = dir_hole_line
    angle_perp_deg = (450 - np.rad2deg(np.arctan2(vec[1], vec[0]))) % 360

    # --- Visualization ---
    image = np.array(image_dict_p4["open_noseat"]).copy()  # load view image

    # Draw points and lines
    cv2.circle(image, inner_top.astype(int), 4, (0, 255, 0), 5)           # green: inner top point
    cv2.circle(image, intersection_point.astype(int), 4, (0, 0, 255), 5)  # red: intersection point
    cv2.line(image, inner_top.astype(int), intersection_point.astype(int), (255, 255, 0), 2)  # yellow: vertical
    cv2.line(image, pt_min.astype(int), pt_max.astype(int), (255, 0, 255), 1)                # magenta: hole width line

    # Add measurement text
    label = f"{dist_px:.1f}px | {dist_cm:.2f}cm | {dist_inch:.2f}in"
    mid = ((inner_top + intersection_point) / 2).astype(int)
    text_pos = (mid[0] + 10, mid[1] - 10)

    # Draw black background box behind text for readability
    overlay = image.copy()
    (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
    rect_start = (text_pos[0] - 10, text_pos[1] - th - 10)
    rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10)
    cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1)
    cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image)

    # Add label text in white
    cv2.putText(image, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)

    # Save resulting image to a PIL object
    buf = BytesIO()
    plt.imsave(buf, image)
    buf.seek(0)
    rim_result_image_p10 = PILImage_p10.open(buf)

    # Text summary output
    rim_to_hole_str = (
        f"Inner Rim to Hole Distance:\n{dist_px:4.1f}px | {dist_cm:4.2f}cm | {dist_inch:4.2f}in\n"
        f"\nRim Direction Angle (Down):\n{angle_down_deg:4.1f}°\n"
        f"\nHole Width Angle:\n{angle_perp_deg:4.1f}°"
    )

    # Return everything needed for Gradio UI
    return (
        gr.update(value=rim_result_image_p10, visible=True),  # image with overlays
        dist_px,            # raw distance in pixels
        dist_cm,            # distance in cm
        dist_inch,          # distance in inches
        angle_down_deg,     # angle of downward rim direction
        angle_perp_deg,     # angle of hole width line
        intersection_point.tolist(),  # intersection point as list
        gr.update(value=rim_to_hole_str, visible=True),       # summary string
        gr.update(visible=True)                               # show result box
    )

# ================================
# 🌀 PART 10: Ellipse-Based Rim Orientation (Closed Lid, Stateless)
# ================================

from PIL import Image as PILImage_p10
from io import BytesIO

def analyze_closed_rim_orientation_p10(_trigger, binary_masks_p4, image_dict_p4):
    # ---------------------------------------------------------------
    # Subfunction to remove top portion of the mask based on angle
    # ---------------------------------------------------------------
    def remove_top_based_on_angle(mask, center, angle_deg, threshold):
        mask = (mask > 0).astype(np.uint8)  # Ensure binary format
        h, w = mask.shape
        cx, cy = center
        angle_rad = np.deg2rad(angle_deg)

        # Direction vector along the angle
        dx = math.cos(angle_rad)
        dy = math.sin(angle_rad)

        # Perpendicular direction (used for computing distances from axis)
        perp_dx = -dy
        perp_dy = dx

        # Iterate over all rows in the mask
        for y in range(h):
            x_coords = np.where(mask[y] == 1)[0]  # Get all foreground pixels in row
            if len(x_coords) == 0:
                continue  # Skip if row is empty

            distances = []
            for x in x_coords:
                px, py = x, y
                dxp = px - cx
                dyp = py - cy
                # Distance of (x,y) from ellipse axis using dot product with perpendicular vector
                dist = abs(dxp * perp_dx + dyp * perp_dy)
                distances.append(dist)

            if max(distances) < threshold:
                # If max distance is small, likely part of the top region → remove
                mask[y, x_coords] = 0
            else:
                # Stop removing when actual rim area is reached
                break

        return mask * 255  # Return as 255-mask

    # ----------------------
    # Step 1: Clean the mask
    # ----------------------
    mask = binary_masks_p4["rim"]["closed"]  # Get closed image rim mask
    center_estimate = (150, 220)  # Approx center for removal reference
    clean_mask = remove_top_based_on_angle(mask, center_estimate, 23, 30)

    # Update the rim mask after cleaning
    binary_masks_p4["rim"]["closed"] = clean_mask

    # --------------------------
    # Step 2: Fit ellipse on mask
    # --------------------------
    mask_u8 = (clean_mask * 255).astype(np.uint8)  # Convert to 8-bit image
    contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    assert contours, "❌ No contours found in closed rim mask!"
    
    rim_contour = max(contours, key=cv2.contourArea)  # Take largest contour
    assert len(rim_contour) >= 5, "❌ Need at least 5 points to fit an ellipse!"

    # Fit ellipse to contour
    ellipse = cv2.fitEllipse(rim_contour)
    (center_x, center_y), (major_axis, minor_axis), angle_deg = ellipse
    ellipse_center = np.array([int(center_x), int(center_y)])  # Center of ellipse

    # ---------------------------
    # Step 3: Calculate directions
    # ---------------------------
    angle_deg += 90  # Rotate so long axis is considered vertical
    angle_rad = math.radians(angle_deg)
    
    # Calculate downward vector (unit vector)
    dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)])
    dir_down /= np.linalg.norm(dir_down)

    # ✅ Ensure dir_down is pointing downward (positive Y direction)
    if dir_down[1] < 0:
        dir_down *= -1

    # Get rightward direction as perpendicular vector to dir_down
    dir_right = np.array([-dir_down[1], dir_down[0]])

    # ✅ Ensure dir_right points rightward (positive X direction)
    if dir_right[0] < 0:
        dir_right *= -1

    # Recompute angle from dir_down to correct it for rendering
    angle_rad_back = math.atan2(dir_down[1], dir_down[0])
    angle_deg = math.degrees(angle_rad_back)

    # Calculate final angle in degrees (adjusted to 0–360° range)
    ellipse_angle_deg = (450 - angle_deg) % 360

    # --------------------------
    # Step 4: Visualize results
    # --------------------------
    image = np.array(image_dict_p4["closed"]).copy()  # Get original closed image
    cv2.circle(image, ellipse_center, 4, (255, 255, 0), 5)  # Draw center point

    # Draw downward direction (green)
    pt_down = (ellipse_center + dir_down * 100).astype(int)
    cv2.arrowedLine(image, ellipse_center, pt_down, (0, 255, 0), 3)

    # Draw rightward direction (cyan)
    pt_right = (ellipse_center + dir_right * 100).astype(int)
    cv2.arrowedLine(image, ellipse_center, pt_right, (0, 255, 255), 3)

    # Convert visualized image to displayable PNG
    plt.figure(figsize=(6, 6))
    plt.imshow(image)
    plt.title("Toilet Rim Orientation using Ellipse Fitting")
    plt.axis("off")
    buf = BytesIO()
    plt.savefig(buf, format="png")
    plt.close()
    buf.seek(0)
    ellipse_viz_image = PILImage_p10.open(buf)

    # --------------------------
    # Step 5: Prepare output text
    # --------------------------
    closed_orientation_str = (
        f"Downward Direction Angle:\n{ellipse_angle_deg:4.1f}°\n"
        f"\nPerpendicular Direction Angle:\n{(ellipse_angle_deg + 90) % 360:4.1f}°\n"
        f"\nEllipse Center:\n({ellipse_center[0]}, {ellipse_center[1]})"
    )

    # --------------------------
    # Step 6: Return results
    # --------------------------
    return (
        gr.update(value=ellipse_viz_image, visible=True),  # Displayed image
        ellipse_angle_deg,                                 # Angle of downward direction
        ellipse_center.tolist(),                           # Center coordinates
        dir_down.tolist(),                                 # Downward unit vector
        dir_right.tolist(),                                # Rightward unit vector
        binary_masks_p4,                                   # Updated binary masks
        gr.update(value=closed_orientation_str, visible=True),  # Text summary
        gr.update(visible=True)                            # Make text box visible
    )

# ================================
# 🧮 PART 11: Rim Height Along Downward Direction (Closed Rim, Stateless)
# ================================

from PIL import Image as PILImage_p11
from io import BytesIO

# Main function to analyze rim height along vertical direction on closed toilet seat
def analyze_rim_height_on_closed_p11(
    _trigger,                          # Dummy trigger input to control Gradio interaction
    binary_masks_p4,                  # Dictionary of segmentation masks (rim masks in this case)
    image_closed_lid_rotated_p3,      # Rotated image for closed-lid condition
    ellipse_angle_deg_p10,            # Angle of fitted ellipse (used to determine vertical direction)
    ref_ratios_p5,                    # Pixel per cm reference for conversion
    rim_height_cm_p8                  # Precomputed rim height (open) from earlier stage
):
    # Utility function to trace along a vector direction from a center point
    # and find the furthest foreground (non-zero) points on both sides of the direction
    def find_extreme_points_along_line(mask, center, direction, max_steps=2000):
        H, W = mask.shape
        pt1 = pt2 = None
        # Move forward from center along direction vector
        for step in range(1, max_steps):
            pt = center + step * direction
            x, y = int(round(pt[0])), int(round(pt[1]))
            if not (0 <= x < W and 0 <= y < H): break
            if mask[y, x] > 0:
                pt2 = np.array([x, y])  # furthest found point
            elif pt2 is not None:
                break  # exit once we leave the foreground area
        # Move backward from center along opposite direction
        for step in range(1, max_steps):
            pt = center - step * direction
            x, y = int(round(pt[0])), int(round(pt[1]))
            if not (0 <= x < W and 0 <= y < H): break
            if mask[y, x] > 0:
                pt1 = np.array([x, y])  # furthest found point
            elif pt1 is not None:
                break
        return pt1, pt2

    # Utility function to calculate Euclidean distance between two points
    def dist(a, b):
        return np.linalg.norm(a - b) if a is not None and b is not None else None

    # --- Step 1: Get mask and image ---
    mask = binary_masks_p4["rim"]["closed"]                     # Binary mask for closed rim
    image = np.array(image_closed_lid_rotated_p3)               # Convert PIL image to NumPy array

    # --- Step 2: Compute center of the mask contour ---
    mask_u8 = (mask * 255).astype(np.uint8)                     # Convert to 8-bit mask for contour detection
    contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    assert contours, "❌ No contours found in closed rim mask!"  # Raise error if no contours
    cnt = max(contours, key=cv2.contourArea)                    # Largest contour
    M = cv2.moments(cnt)                                        # Calculate image moments
    cx = int(M["m10"] / M["m00"])                               # X coordinate of centroid
    cy = int(M["m01"] / M["m00"])                               # Y coordinate of centroid
    center = np.array([cx, cy])                                 # Center point

    # --- Step 3: Determine direction vector using ellipse angle ---
    angle_rad = np.deg2rad((450 - ellipse_angle_deg_p10) % 360) # Convert to radian (corrected for rotation)
    dir_vec = np.array([math.cos(angle_rad), math.sin(angle_rad)])  # Unit vector in direction

    # --- Step 4: Trace extreme points along the direction vector ---
    pt_start, pt_end = find_extreme_points_along_line(mask, center, dir_vec)
    rim_height_px = dist(pt_start, pt_end)                      # Height in pixels
    px_per_cm = ref_ratios_p5['closed']                         # Pixel-per-cm ratio
    full_rim_height_cm = rim_height_px / px_per_cm              # Total rim height in cm
    full_rim_height_in = full_rim_height_cm / 2.54              # Convert to inches

    # --- Step 5: Use rim height from open seat to compute overlap ---
    rim_height_cm = rim_height_cm_p8                            # Rim height from open seat (in cm)
    rim_height_in = rim_height_cm / 2.54                        # Convert to inches

    closed_remaining_cm = full_rim_height_cm - rim_height_cm    # Height still covered by lid
    closed_remaining_in = closed_remaining_cm / 2.54            # Convert to inches

    # --- Step 6: Visualize the result if both points were found ---
    if pt_start is not None and pt_end is not None:
        vis = image.copy()
        # Draw points and line on visualization
        cv2.circle(vis, tuple(center), 4, (255, 255, 0), -1)         # center (yellow)
        cv2.circle(vis, tuple(pt_start), 5, (0, 0, 255), -1)         # start point (red)
        cv2.circle(vis, tuple(pt_end), 5, (0, 255, 0), -1)           # end point (green)
        cv2.line(vis, tuple(pt_start), tuple(pt_end), (0, 255, 255), 2)  # vertical line (cyan)

        # Create overlay label
        label = f"{rim_height_px:.1f}px | {full_rim_height_cm:.2f}cm | {full_rim_height_in:.2f}in"
        mid_point = ((pt_start + pt_end) / 2).astype(int)
        text_pos = (mid_point[0] + 10, mid_point[1] - 10)

        # Create label background box
        overlay = vis.copy()
        (text_w, text_h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2)
        rect_start = (text_pos[0] - 10, text_pos[1] - text_h - 10)
        rect_end = (text_pos[0] + text_w + 10, text_pos[1] + 10)
        cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1)  # background
        cv2.addWeighted(overlay, 0.7, vis, 0.3, 0, vis)              # apply overlay
        cv2.putText(vis, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)

        # Save visualization to in-memory image
        plt.figure(figsize=(6, 6))
        plt.imshow(vis)
        plt.title("Rim Height on Closed Lid")
        plt.axis("off")
        buf = BytesIO()
        plt.savefig(buf, format='png')
        plt.close()
        buf.seek(0)
        rim_height_vis = PILImage_p11.open(buf)              # PIL Image for output
        
        pt_start_p11 = pt_start                              # Store start point

        # Final string to show in UI
        rim_closed_str = (
            f"Total Height:\n{rim_height_px:4.1f}px | {full_rim_height_cm:4.2f}cm | {full_rim_height_in:4.2f}in"
        )

        # Return all values to Gradio UI
        return (
            gr.update(value=rim_height_vis, visible=True),  # Updated visualization
            rim_height_cm,                                  # Open rim height (cm)
            rim_height_in,                                  # Open rim height (in)
            full_rim_height_cm,                             # Closed rim full height (cm)
            full_rim_height_in,                             # Closed rim full height (in)
            closed_remaining_cm,                            # Height still hidden by lid (cm)
            closed_remaining_in,                            # Height still hidden by lid (in)
            pt_start_p11,                                   # Topmost point on closed rim
            gr.update(value=rim_closed_str, visible=True),  # Text summary for UI
            gr.update(visible=True)                         # Enable output group in Gradio
        )

    else:
        # If points couldn't be found, return empty outputs
        print("⚠️ Could not find valid intersection points.")
        return (None, None, None, None, None, None, None, None, None, None)

# ================================
# 🧱 PART 12: Draw Remaining Closed Lid Portion (Stateless)
# ================================

def draw_remaining_closed_portion_p12(
    _trigger,
    pt_start_p11,                  # Starting point from Part 11 (top of closed rim)
    closed_remaining_cm_p11,       # Remaining distance (in cm) to be drawn
    ref_ratios_p5,                 # Dictionary with px/cm ratios for all image types
    ellipse_dir_down_p10,          # Unit vector pointing downward from ellipse analysis (Part 10)
    image_closed_lid_rotated_p3    # The closed lid image, rotated for alignment
):
    # --- Clone and compute points ---
    pt_start = np.array(pt_start_p11)            # Convert input start point to NumPy array
    dir_down = np.array(ellipse_dir_down_p10)    # Direction unit vector from ellipse (downward)
    remaining_cm = closed_remaining_cm_p11       # Distance to draw, in cm
    px_per_cm = ref_ratios_p5['closed']          # Get pixel-to-cm conversion for closed image
    remaining_px = remaining_cm * px_per_cm      # Convert remaining cm to pixels
    pt_end = (pt_start + dir_down * remaining_px).astype(int)  # Compute end point using direction and distance

    # --- Compute angle ---
    vec = pt_end - pt_start                      # Vector between start and end point
    angle_rad = np.arctan2(vec[1], vec[0])       # Angle in radians using arctangent of vector
    remaining_angle_deg = (450 - np.rad2deg(angle_rad)) % 360  # Convert to clockwise angle in degrees (0° at top)

    # --- Draw on image ---
    image = np.array(image_closed_lid_rotated_p3).copy()  # Make a copy of the rotated image to draw on
    cv2.line(image, tuple(pt_start), tuple(pt_end), (0, 0, 255), 3)  # Draw red line from start to end
    cv2.circle(image, tuple(pt_start), 5, (0, 255, 0), -1)  # Draw green circle at start
    cv2.circle(image, tuple(pt_end), 5, (255, 0, 0), -1)    # Draw blue circle at end

    # --- Add label ---
    remaining_in = remaining_cm / 2.54                       # Convert cm to inches
    label = f"({remaining_cm:.2f} cm) | ({remaining_in:.2f} in)"  # Create label with both units
    text_pos = (pt_end[0] + 10, pt_end[1] - 10)              # Position label near end point
    cv2.putText(image, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX,
                1, (0, 0, 255), 2, cv2.LINE_AA)              # Draw label text in red

    # --- Compose info string for UI ---
    closed_remaining_str = (
        f"Remaining Portion Length:\n{remaining_cm:4.2f}cm | {remaining_in:4.2f}in\n"
        f"\nDirection Angle:\n{remaining_angle_deg:4.1f}°"
    )

    # --- Return image + values for display ---
    return (
        gr.update(value=(PILImage_p11.fromarray(image)), visible=True),  # Show annotated image
        remaining_angle_deg,      # Angle of the drawn segment
        pt_start,                 # Start point
        pt_end,                   # End point
        gr.update(value=closed_remaining_str, visible=True),  # Display stats string
        gr.update(visible=True)  # Make result components visible
    )

# ================================
# 🧱 PART 13: Top Rim Width Measurement (Stateless)
# ================================

def analyze_top_rim_width_p13(
    _trigger,
    binary_masks_p4,                 # Dictionary containing binary masks, here we use the 'rim' mask for the closed lid image
    ellipse_center_p10,             # Center of fitted ellipse (from Part 10)
    ellipse_angle_deg_p10,         # Angle of fitted ellipse in degrees (from Part 10)
    ref_ratios_p5,                  # Reference px/cm ratios (from Part 5)
    image_closed_lid_rotated_p3    # Rotated closed-lid image for overlay (from Part 3)
):
    import matplotlib.pyplot as plt
    from PIL import Image as PILImage_p13
    from io import BytesIO

    # Extract the necessary variables from inputs
    mask = binary_masks_p4['rim']['closed']
    center = np.array(ellipse_center_p10)
    angle_deg = (450 - ellipse_angle_deg_p10) % 360  # Adjust angle for image coordinate system
    px_per_cm = ref_ratios_p5['closed']
    image = np.array(image_closed_lid_rotated_p3).copy()  # Convert PIL image to NumPy array

    # Bresenham’s line algorithm for drawing a line between two points
    def bresenham_line(x0, y0, x1, y1):
        points = []
        steep = abs(y1 - y0) > abs(x1 - x0)
        if steep: x0, y0, x1, y1 = y0, x0, y1, x1
        swapped = False
        if x0 > x1:
            x0, x1 = x1, x0
            y0, y1 = y1, y0
            swapped = True
        dx = x1 - x0
        dy = abs(y1 - y0)
        error = dx / 2
        ystep = 1 if y0 < y1 else -1
        y = y0
        for x in range(x0, x1 + 1):
            pt = (y, x) if steep else (x, y)
            points.append(pt)
            error -= dy
            if error < 0:
                y += ystep
                error += dx
        if swapped: points.reverse()
        return points

    # Core logic to find the topmost rim point and the width across it
    def find_top_and_perpendicular_extremes(mask, center, angle_deg, perp_halfwidth=7000, max_up_scan=6500):
        mask = (mask > 0).astype(np.uint8)  # Ensure binary mask
        h, w = mask.shape
        angle_rad = np.deg2rad(angle_deg)
        dx, dy = np.cos(angle_rad), np.sin(angle_rad)

        # Upward direction is negative direction of ellipse angle
        up_dir = np.array([-dx, -dy])
        # Perpendicular to rim direction (cross-section)
        perp_dir = np.array([-dy, dx])

        pt_top = None
        # Scan upward from center pixel to find the first mask pixel
        for i in range(max_up_scan):
            pt = center + up_dir * i
            x, y = int(round(pt[0])), int(round(pt[1]))
            if 0 <= x < w and 0 <= y < h and mask[y, x] == 1:
                pt_top = (x, y)
        if pt_top is None:
            raise ValueError("No mask pixel found when scanning upward from center.")

        # Slight offset downward for more reliable width reading
        pt_top = (pt_top[0], int(pt_top[1] + h * 0.02))

        # Generate line endpoints left and right of pt_top along the perpendicular axis
        left_pt = (int(round(pt_top[0] - perp_dir[0] * perp_halfwidth)),
                   int(round(pt_top[1] - perp_dir[1] * perp_halfwidth)))
        right_pt = (int(round(pt_top[0] + perp_dir[0] * perp_halfwidth)),
                    int(round(pt_top[1] + perp_dir[1] * perp_halfwidth)))

        # Draw a line across the rim at the top point to find edges
        line_pts = bresenham_line(left_pt[0], left_pt[1], right_pt[0], right_pt[1])
        valid_pts = [pt for pt in line_pts if 0 <= pt[0] < w and 0 <= pt[1] < h and mask[pt[1], pt[0]] == 1]

        if len(valid_pts) < 2:
            raise ValueError("Not enough mask pixels found along perpendicular line.")

        # Take first and last valid pixels on rim as width endpoints
        pt1, pt2 = valid_pts[0], valid_pts[-1]
        dist_px = np.linalg.norm(np.array(pt2) - np.array(pt1))
        return pt_top, pt1, pt2, dist_px

    # Run the function to find points and distance
    pt_top, pt1, pt2, dist_px = find_top_and_perpendicular_extremes(mask, center, angle_deg)

    # Convert from pixels to cm and inches
    dist_cm = dist_px / px_per_cm
    dist_in = dist_cm / 2.54

    # Calculate angle of this actual width vector for reference
    vec = np.array(pt2) - np.array(pt1)
    angle_rad_actual = np.arctan2(vec[1], vec[0])
    angle_deg_actual = (450 - np.rad2deg(angle_rad_actual)) % 360

    # -------------------------------
    # 🖼 Visualization for feedback
    # -------------------------------
    fig, ax = plt.subplots(figsize=(8, 8))
    ax.imshow(image)  # Show image
    ax.plot([pt1[0], pt2[0]], [pt1[1], pt2[1]], 'r-', linewidth=1)  # Draw width line
    ax.scatter(*pt1, color='lime', s=20)  # Start point
    ax.scatter(*pt2, color='cyan', s=20)  # End point

    # Display measurements in the middle of the line
    mid_x = (pt1[0] + pt2[0]) / 2
    mid_y = (pt1[1] + pt2[1]) / 2
    ax.text(mid_x, mid_y + 100, f"{dist_px:.1f}px | {dist_cm:.2f}cm | {dist_in:.2f}in",
            fontsize=10, color='white', bbox=dict(facecolor='black', alpha=0.6))
    ax.set_title("Top Rim Width Measurement")
    ax.axis('off')

    # Save plot as image
    buf = BytesIO()
    plt.savefig(buf, format='png')
    plt.close()
    buf.seek(0)
    vis_image = PILImage_p13.open(buf)

    # Return readable output string for textbox
    top_rim_str = (
        f"Top Rim Width:\n{dist_px:4.1f}px | {dist_cm:4.2f}cm | {dist_in:4.2f}in\n"
        f"\nOrientation Angle:\n{angle_deg_actual % 180:4.1f}°"
    )

    # Return everything required for UI
    return (
        gr.update(value=vis_image, visible=True),  # Output image
        pt_top,                                     # Topmost point
        pt1, pt2,                                   # Width line endpoints
        dist_px, dist_cm, dist_in,                  # Measurements
        angle_deg_actual,                           # Measured orientation
        gr.update(value=top_rim_str, visible=True), # Text output
        gr.update(visible=True)                     # Control for showing card or section
    )
# ================================
# 🧩 FINAL PART: Combined App Launcher
# ================================
# This part contains logic for launching the final combined Gradio app,
# including Supabase authentication, file upload functionality, and login logic.

import gradio as gr
import os
from supabase import create_client, Client
from datetime import datetime

# ========== Supabase Configuration ==========
# Load environment variables for Supabase project
# SUPABASE_URL = os.getenv("SUPABASE_URL")  # Supabase project URL
# SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")  # Service role key
# SUPABASE_BUCKET = os.getenv("SUPABASE_BUCKET_NAME")  # Bucket name to upload ZIPs

# # Check if keys are present, else raise an error
# if not SUPABASE_URL or not SUPABASE_KEY:
    # raise Exception("Supabase keys not set properly!")

# # Create the Supabase client
# supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)

# ===== Upload Function: App Output ZIP =====
def upload_zip(email, zip_file):
    # Validate presence of both email and file
    if not email or not zip_file:
        print("❌ Email and ZIP file are required.")
        return gr.update(visible=True)

    # Ensure it's a .zip file
    if not zip_file.name.endswith('.zip'):
        print("❌ Only ZIP files are allowed.")
        return gr.update(visible=True)

    # Create a unique filename using email and timestamp
    timestamp = datetime.now().isoformat().replace(":", "-").split(".")[0]
    safe_email = email.replace("@", "_at_").replace(".", "_")
    filename = f"{safe_email}_{timestamp}.zip"
    path_in_bucket = f"zips/{filename}"  # Folder path inside the bucket

    # Read uploaded file bytes
    with open(zip_file.name, "rb") as f:
        file_bytes = f.read()

    # Upload file to Supabase storage
    response = supabase.storage.from_(SUPABASE_BUCKET).upload(
        path_in_bucket, file_bytes, {"content-type": "application/zip"}
    )

    # Log upload response
    print(response)
    return gr.update(visible=True)

# ===== Upload Function: Error ZIP Upload =====
def upload_zip_error(email, zip_file):
    # Check email and zip are both provided
    if not email or not zip_file:
        print("❌ Email and ZIP file are required.")
        return

    # Ensure it's a ZIP file
    if not zip_file.name.endswith('.zip'):
        print("❌ Only ZIP files are allowed.")
        return

    # Generate timestamped safe filename
    timestamp = datetime.now().isoformat().replace(":", "-").split(".")[0]
    safe_email = email.replace("@", "_at_").replace(".", "_")
    filename = f"error_analysis_{safe_email}_{timestamp}.zip"
    path_in_bucket = f"zips/{filename}"  # Target location in bucket

    # Read file as binary
    with open(zip_file.name, "rb") as f:
        file_bytes = f.read()

    # Upload the error ZIP to Supabase storage
    response = supabase.storage.from_(SUPABASE_BUCKET).upload(
        path_in_bucket, file_bytes, {"content-type": "application/zip"}
    )

    # Print result to console
    print(response)
    return

# ===== Supabase Login Authentication =====
# def supa_login(email, password):
    # try:
        # # Query Supabase users table for user with given email
        # result = supabase.table("users").select("*").eq("email", email).execute()
        # users = result.data

        # # If no such user exists
        # if not users:
            # return "❌ No user found with that email", False

        # # Validate password
        # user = users[0]
        # if user["password"] == password:
            # return "✅ Login successful!", True
        # else:
            # return "❌ Incorrect password", False
    # except Exception as e:
        # # Handle any exceptions that occur during login
        # return f"❌ Login error: {e}", True

# Import necessary libraries
import matplotlib.pyplot as plt  # For plotting
import matplotlib.patches as patches  # For drawing shapes like polygons and ellipses
import numpy as np  # For numerical computations
from PIL import Image  # For image saving/loading as PIL object
import os  # For file operations

# ================================
# 🟫 Define chassis image data
# ================================
my_polygons = [
    {
        'points': [(0.56, 14.72), (0.56, 19.05), (19.5, 19.05), (19.5, 14.53)],
        'facecolor': "#E6ADAD",     # Light red fill
        'edgecolor': 'brown',       # Border color (not visible if linewidth is 0)
        'linewidth': 0,             # No border
        'zorder': 2                 # Renders above background but below zorder=3 or 4
    },
    {
        'points': [(3, 14.72), (3, 9.45), (17, 9.45), (17, 14.6)],
        'facecolor': '#E6ADAD',     # Same fill as above
        'edgecolor': 'darkgreen',
        'linewidth': 0,
        'zorder': 1                 # Rendered below most elements
    },
    {
        'points': [(4.24, 18.74), (9.68, 18.74), (9.68, 17.34), (4.24, 17.34)],
        'facecolor': '#f0f0f0',     # Matches background
        'edgecolor': 'brown',
        'linewidth': 0,
        'zorder': 4                 # Highest priority, renders on top
    },
    {
        'points': [(12.25, 18.74), (15.88, 18.74), (15.88, 17.34), (12.25, 17.34)],
        'facecolor': '#f0f0f0',
        'edgecolor': 'darkgreen',
        'linewidth': 0,
        'zorder': 4                 # Also renders on top
    }
]

# ================================
# 🟡 Define chassis image data
# ================================
my_ellipses = [
    {
        'center_x': 10.0, 'center_y': 9.45,
        'width': 9.0, 'height': 12.0, 'angle': 0,
        'facecolor': '#f0f0f0',   # Same as background
        'edgecolor': 'blue',
        'linewidth': 0,
        'zorder': 4              # Render on top of all lower z-order shapes
    },
    {
        'center_x': 10.0, 'center_y': 9.45,
        'width': 14.0, 'height': 17.0, 'angle': 0,
        'facecolor': '#E6ADAD',  # Light red
        'edgecolor': 'purple',
        'linewidth': 0,
        'zorder': 3              # Under the inner ellipse but above most polygons
    }
]

def generate_reload_js(email, password):
    # JS that reloads the page with email and password in the hash
    email_enc = urllib.parse.quote(email)
    password_enc = urllib.parse.quote(password)
    return gr.update(_js=f"() => location.href = location.origin + location.pathname + '#email={email_enc}&password={password_enc}'")

# ================================
# 🚀 Final App Launch Logic
# ================================
# Preload email/password if found in URL fragment (e.g., #email=...&password=...)
def restore_from_url(url):
    parsed = urllib.parse.urlparse(url)
    query = urllib.parse.parse_qs(parsed.fragment or parsed.query)
    email = query.get("email", [""])[0]
    password = query.get("password", [""])[0]
    return email, password

def launch_main_app():
    import gradio as gr
    import urllib.parse
    url_box = gr.Textbox(visible=False)
    
    email = gr.Textbox(value="no_login_user", visible=False)
    password = gr.Textbox(value="", visible=False)

    
    # On app load, prefill email/password from URL
    #full_app_interface.load(restore_from_url, [url_box], [email, password])

    # Main container column
    with gr.Column("🚀 Main App"):

        # --- Row for Logout Button ---
        with gr.Row(elem_id="logout-row"):
            logout_btn = gr.Button("Logout", elem_id="logout-btn")
        
        # JS to reload the app on logout click (clears session)
        logout_btn.click(fn=None, js="() => location.reload()")

        # --- App Title and Intro ---
        gr.Markdown("# 🚽 Smart Toilet Image Checker", elem_id="centered-title")
        gr.Markdown("Just upload your toilet photos — then sit back and watch the AI work its magic!", elem_id="centered-title")
        gr.Markdown("📘 **New here?** If you're not sure how to use this app, please check out the [step-by-step instructions](https://drive.google.com/file/d/1qCwzaePLexOF8Ti-O-bhC-oPsB14lbFj/view?usp=sharing).")

        # Create a Gradio session state for current user interaction
        session_state = gr.State(init_session())

        # --- Example Upload Section ---
        with gr.Row():
            gr.Markdown("### 📷 How to Upload Images Correctly (Example)")

        # Show 3 wrong upload examples and 1 correct example
        with gr.Row():
            with gr.Column():
                gr.Markdown("❌ Wrong")
                wrong1 = gr.Image(value="./static/im1.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False)
            with gr.Column():
                gr.Markdown("❌ Wrong")
                wrong2 = gr.Image(value="./static/im2.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False)
            with gr.Column():
                gr.Markdown("❌ Wrong")
                wrong3 = gr.Image(value="./static/im3.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False)

        # Display correct upload example in center
        with gr.Row():
            with gr.Column():
                z = "filler"  # Just spacing
            with gr.Column():
                gr.Markdown("✅ Correct")
                correct = gr.Image(value="./static/im4.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False)
            with gr.Column():
                z = "filler"

        # --- Session-wide Global State Declarations (Gradio-safe) ---
        # These maintain state across tab interactions

        # Part 4
        binary_masks_p4 = gr.State()
        image_dict_p4 = gr.State()

        # Part 5
        out_ref_ratios_p5 = gr.State()

        # Part 7 (Rim ellipse measurements)
        out_rimellipse_ui_p7 = gr.State()
        out_rimellipse_cm_p7 = gr.State()
        out_rimellipse_inch_p7 = gr.State()
        inner_top_p7 = gr.State()
        dir_down_p7 = gr.State()
        dir_right_p7 = gr.State()

        # Part 8
        btn_rimheight_p8 = gr.State()
        rimheight_text_p8 = gr.State()
        rim_height_px_p8 = gr.State()
        rim_height_cm_p8 = gr.State()
        rim_height_inch_p8 = gr.State()
        inner_top_p8 = gr.State()

        # Part 9
        btn_measure_holewidth_p9 = gr.State()
        hole_width_px_p9 = gr.State()
        hole_width_cm_p9 = gr.State()
        hole_width_inch_p9 = gr.State()
        angle_deg_p9 = gr.State()
        pt_min_p9 = gr.State()
        pt_max_p9 = gr.State()

        # Part 10
        btn_top_to_hole_p10 = gr.State()
        top_to_hole_line_px_p10 = gr.State()
        top_to_hole_line_cm_p10 = gr.State()
        top_to_hole_line_inch_p10 = gr.State()
        angle_down_deg_p10 = gr.State()
        angle_perp_deg_p10 = gr.State()
        intersection_point_p10 = gr.State()
        btn_ellipse_orient_p10 = gr.State()
        ellipse_angle_deg_p10 = gr.State()
        ellipse_center_p10 = gr.State()
        ellipse_dir_down_p10 = gr.State()
        ellipse_dir_right_p10 = gr.State()

        # Part 11 (Closed lid rim)
        updated_binary_masks_p4 = gr.State()
        btn_rim_height_closed_p11 = gr.State()
        rim_height_cm_p11 = gr.State()
        rim_height_in_p11 = gr.State()
        full_rim_height_cm_p11 = gr.State()
        full_rim_height_in_p11 = gr.State()
        closed_remaining_cm_p11 = gr.State()
        closed_remaining_in_p11 = gr.State()
        pt_start_p11 = gr.State()

        # Part 12 (Remaining portion drawing)
        btn_draw_remaining_p12 = gr.State()
        remaining_angle_deg_p12 = gr.State()
        pt_start_p12 = gr.State()
        pt_end_p12 = gr.State()

        # Generic measurement tool
        btn_measure = gr.State()
        out_pt_top = gr.State()
        out_pt1 = gr.State()
        out_pt2 = gr.State()
        out_dist_px = gr.State()
        out_dist_cm = gr.State()
        out_dist_in = gr.State()
        out_angle_deg = gr.State()

        # Part 6 rim overlay results
        out_rim_measurements_p6 = gr.State()
        out_rim_measurements_cm_p6 = gr.State()
        out_rim_measurements_inch_p6 = gr.State()

        # Part 2 Models (Segmentation models)
        models_holes_p2 = gr.State(None)
        models_rim_p2 = gr.State(None)
        models_coinref_p2 = gr.State(None)
        device_p2 = gr.State(None)

        # Optional debugging masks (commented)
        gallery_segmentation_p4 = gr.State()

        # For previewing polygons/ellipses in mask viewer
        polygons, ellipses = gr.State(), gr.State()
        polygons1, ellipses1 = gr.State(), gr.State()

        # --- Load models and device globally at app load ---
        full_app_interface.load(
            fn=lambda: [GLOBAL_HOLES, GLOBAL_RIM, GLOBAL_COIN, device_p2],  # Initialize models and device
            outputs=[models_holes_p2, models_rim_p2, models_coinref_p2, device_p2],
            queue=False  # Avoid blocking queue during load
        )
        
        # Injecting custom CSS into the Gradio app using gr.HTML
        gr.HTML("""<style>
                    /* Center-align title text */
                    #centered-title {
                        text-align: center;
                        width: 100%
                    }
                    /* Style for footer text */
                    #footer-text {
                        text-align: center;
                        font-size: 14px;
                        color: #666;
                        margin-top: 40px;
                        padding-top: 20px;
                        border-top: 1px solid #ddd;
                        line-height: 1.6
                    }
                    /* Custom styling for textarea input (monospace box) */
                    #pretty-box textarea {
                        font-size: 16px;
                        font-family: monospace;
                        border: 2px solid #ccc;
                        border-radius: 12px;
                        padding: 10px 14px;
                        resize: none;
                        height: 50px;
                        line-height: 1.4;
                        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
                        min-width: 300px;
                        max-width: 150%;
                    }
                    /* Label styling inside the pretty-box */
                    #pretty-box label {
                        text-align: center;
                        font-size: 18px;
                    }
                    /* Utility box for fitting controls */
                    #fit-box {
                        min-width: 200px;
                        margin: auto;
                    }
                    /* Override Gradio block styling to make background transparent */
                    .gr-block.gr-group {
                        background-color: inherit !important;
                        padding: 16px;
                    }
                    #custom_footer {
                        position: fixed;
                        left: 0;
                        right: 0;
                        bottom: 0;
                        width: 100%;
                        padding: 10px 20px;
                    }
                    /* Add bottom margin/padding to main content so it's not overlapped */
                    body, .gradio-container, #root {
                        padding-bottom: 150px; /* Adjust based on your footer's height */
                    }
                    footer[class*="svelte"] {
                        display: none !important;
                    }
                    #footer-contact, #footer-left, #footer-right {
                        font-size: 14px;
                        line-height: 1.5;
                    }
                    #footer-contact a,
                    #footer-right a {
                        color: #61dafb;  /* Stylish light-blue link color */
                        text-decoration: none;
                    }
                    #footer-contact a:hover,
                    #footer-right a:hover {
                        text-decoration: underline;
                    }
                    /* Logout row positioned to the right */
                    #logout-row {
                        display: flex;
                        justify-content: flex-end;  /* ✅ Push content to the right */
                        padding: 10px;
                    }
                    /* Style for logout button */
                    #logout-btn {
                        background-color: #ef4444;
                        color: white;
                        font-weight: bold;
                        padding: 8px 16px;
                        border-radius: 6px;
                        max-width: 100px;
                        text-align: center;
                    }
                </style>""")
        
        # ===============================================
        # 🔄 Function to rotate an uploaded image live
        # ===============================================
        def rotate_image_live(img):
            # If no image is provided, return None
            if img is None:
                return None

            # Import required libraries
            import numpy as np
            import cv2
            from PIL import Image

            # Convert PIL image to NumPy array
            img_np = np.array(img)

            # Get height and width of the image
            h, w = img_np.shape[:2]

            # Rotate image 90 degrees clockwise
            rotated = cv2.rotate(img_np, cv2.ROTATE_90_CLOCKWISE)

            # Convert back to PIL image and return
            return Image.fromarray(rotated)

        import gradio as gr
        from PIL import Image
        import os

        # Layout section with 3 image upload columns (for different toilet conditions)
        with gr.Row():
            with gr.Column():
                gr.Markdown("### 📸 Open Toilet (No Seat)")  # Section heading
                uploader1 = gr.UploadButton("Upload Image", file_types=["image"])  # Upload button
                input1 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False)  # Image display (initially hidden)
                filename1 = gr.Markdown()  # To show filename
                rotte1 = gr.Button("Rotate", visible=False)  # Rotate button (initially hidden)

            with gr.Column():
                gr.Markdown("### 📸 Open Toilet (With Seat)")
                uploader2 = gr.UploadButton("Upload Image", file_types=["image"])
                input2 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False)
                filename2 = gr.Markdown()
                rotte2 = gr.Button("Rotate", visible=False)

            with gr.Column():
                gr.Markdown("### 📸 Closed Lid Toilet")
                uploader3 = gr.UploadButton("Upload Image", file_types=["image"])
                input3 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False)
                filename3 = gr.Markdown()
                rotte3 = gr.Button("Rotate", visible=False)

        # Utility function to resize an image while keeping aspect ratio
        def resize_keep_aspect(image, target_size):
            """
            Resize image to fit within target_size (width, height),
            keeping aspect ratio. Returns same format (PIL.Image or numpy).
            """
            is_numpy = isinstance(image, np.ndarray)  # Check input type
            if is_numpy:
                image = Image.fromarray(image)  # Convert to PIL if needed

            image = image.copy()  # Avoid modifying original
            image.thumbnail(target_size, Image.LANCZOS)  # Resize with high quality filter

            if is_numpy:
                return np.array(image)  # Return in original format
            else:
                return image

        # Function to handle uploaded image, resize, and extract filename
        def load_image_and_filename(file_obj):
            if file_obj is None:
                return None, "", gr.update(visible=False), gr.update(visible=False)  # If no file, return nothing and hide widgets

            filepath = file_obj.name
            img = Image.open(filepath)
            img = resize_keep_aspect(img, (960, 1280))  # Resize image to max bounds

            filename = os.path.basename(filepath)  # Extract name from path
            return img, f"📁 Filename: {filename}", gr.update(visible=True), gr.update(visible=True)

        # Set upload callbacks for all 3 image uploaders
        uploader1.upload(load_image_and_filename, inputs=uploader1, outputs=[input1, filename1, input1, rotte1])
        uploader2.upload(load_image_and_filename, inputs=uploader2, outputs=[input2, filename2, input2, rotte2])
        uploader3.upload(load_image_and_filename, inputs=uploader3, outputs=[input3, filename3, input3, rotte3])

        # Rotate buttons for each image — rotate image when clicked
        rotte1.click(fn=rotate_image_live, inputs=[input1], outputs=input1)
        rotte2.click(fn=rotate_image_live, inputs=[input2], outputs=input2)
        rotte3.click(fn=rotate_image_live, inputs=[input3], outputs=input3)

        output = gr.State()  # Placeholder for later step output (stateful variable)

        # =========================
        # Next Step Trigger Section
        # =========================

        # Main button to trigger the full pipeline
        with gr.Row():
            run_pipeline_btn = gr.Button("🧠 'Let AI Do the Work'")  # Calls processing logic

        # Processing status placeholder
        with gr.Row():
            process = gr.Markdown("")  # Will show dynamic messages/status

        # Hidden output section (only shown after pipeline is run)
        with gr.Column(visible=False) as group_to_show:
            
            # Coin reference detection result header
            with gr.Row():
                ref = gr.Markdown("# Coin Reference Detection:", elem_id="centered-title", visible=False)

            # Output image for coin reference detection
            with gr.Row():
                out_ref_image_p5 = gr.Image(
                    label="🪙 Coin Reference Detection", 
                    show_share_button=False, 
                    show_fullscreen_button=False, 
                    height=600, 
                    width=800, 
                    visible=False, 
                    interactive=False, 
                    show_download_button=False, 
                    container=False
                )

            # Nicely formatted textbox for coin reference ratio value
            with gr.Row():
                with gr.Column():
                    z = "filler"  # Just padding / alignment element
                with gr.Column(elem_id="fit-box"):
                    ref_ratios_str = gr.Textbox(
                        label="🪙 Coin Reference Ratios", 
                        interactive=False, 
                        elem_id='pretty-box', 
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

            # ---------- Seat Dimensions ----------
            with gr.Row():  # Title row for seat dimensions
                seat = gr.Markdown("# Seat Dimensions:", elem_id="centered-title", visible=False)

            with gr.Row():  # Image row for rim width (seat view)
                out_rim_image_p6 = gr.Image(
                    label="📏 Rim Width",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Centered measurement display for seat
                with gr.Column():  # Left spacer
                    z = "filler"
                with gr.Column(elem_id="fit-box"):  # Center column for measurement text
                    seat_measurement_str = gr.Textbox(
                        label="📏 Seat Dimensions:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():  # Right spacer
                    z = "filler"

            # ---------- Rim Dimensions ----------
            with gr.Row():  # Title row for rim dimensions
                rim = gr.Markdown("# Rim Dimensions:", elem_id="centered-title", visible=False)

            with gr.Row():  # Image showing ellipse-based rim dimensioning
                out_rimellipse_image_p7 = gr.Image(
                    label="📏 Rim Dimensions",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Centered measurement display for rim dimensions
                with gr.Column():  # Left spacer
                    z = "filler"
                with gr.Column(elem_id="fit-box"):  # Measurement text
                    rim_measurement_str = gr.Textbox(
                        label="📏 Rim Dimensions:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():  # Right spacer
                    z = "filler"

            # ---------- Rim Height (Inner Top to Outer Bottom) ----------
            with gr.Row():  # Title row for height from inner to outer rim
                inlen = gr.Markdown("# Length of Rim (Inner Top to Outer Bottom):", elem_id="centered-title", visible=False)

            with gr.Row():  # Red line visualization image
                rimheight_image_p8 = gr.Image(
                    label="📏 Red Line Rim",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Measurement display for rim height
                with gr.Column():
                    z = "filler"
                with gr.Column(elem_id="fit-box"):
                    rim_height_str = gr.Textbox(
                        label="📏 Rim Height:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

            # ---------- Hole Width ----------
            with gr.Row():  # Title row for hole width
                hw = gr.Markdown("# Hole Width:", elem_id="centered-title", visible=False)

            with gr.Row():  # Image showing hole width
                holewidth_image_p9 = gr.Image(
                    label="🕳️ Hole Width",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Measurement display for hole width
                with gr.Column():
                    z = "filler"
                with gr.Column(elem_id="fit-box"):
                    hole_width_str = gr.Textbox(
                        label="🕳️ Hole Width:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

            # ---------- Rim to Hole Top Distance ----------
            with gr.Row():  # Title for rim to hole top
                hr = gr.Markdown("# Distance from Holes to Top of Inner Rim:", elem_id="centered-title", visible=False)

            with gr.Row():  # Image showing arrow from hole to rim top
                rim_to_hole_img_p10 = gr.Image(
                    label="⬇️ Rim Line (Open)",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Measurement display for rim to hole distance
                with gr.Column():
                    z = "filler"
                with gr.Column(elem_id="fit-box"):
                    hole_to_top_str = gr.Textbox(
                        label="⬇️ Rim to Hole Dimensions:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

            # ---------- Closed Lid Direction ----------
            with gr.Row():  # Title for toilet orientation
                cl = gr.Markdown("# Direction of Closed Lid Toilet:", elem_id="centered-title", visible=False)

            with gr.Row():  # Arrows on ellipse image
                ellipse_viz_image_p10 = gr.Image(
                    label="🔄 Ellipse Arrows",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Measurement display for toilet orientation
                with gr.Column():
                    z = "filler"
                with gr.Column(elem_id="fit-box"):
                    direction_str = gr.Textbox(
                        label="🔄 Direction:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

            # ---------- Total Height ----------
            with gr.Row():  # Title for total toilet height
                th = gr.Markdown("# Total Height of Entire Toilet:", elem_id="centered-title", visible=False)

            with gr.Row():  # Image showing toilet height on closed lid
                rim_height_vis_p11 = gr.Image(
                    label="📏 Rim Height on Closed",
                    height=600,
                    width=800,
                    show_share_button=False,
                    show_fullscreen_button=False,
                    show_download_button=False,
                    interactive=False,
                    visible=False,
                    container=False,
                )

            with gr.Row():  # Measurement display for total height
                with gr.Column():
                    z = "filler"
                with gr.Column(elem_id="fit-box"):
                    total_height_str = gr.Textbox(
                        label="📏 Total Height:",
                        interactive=False,
                        elem_id='pretty-box',
                        visible=False
                    )
                with gr.Column():
                    z = "filler"

			# Title for remaining lid to holes measurement
            with gr.Row():
                tt = gr.Markdown("# Distance from Top Portion of Toilet to Holes:", elem_id="centered-title", visible=False)

			# Display image showing remaining lid portion
            with gr.Row():
                remaining_lid_img_p12 = gr.Image(label="📏 Remaining Lid Portion", show_share_button=False, show_fullscreen_button=False, height=600, width=800, visible=False, interactive=False, show_download_button=False, container=False)

			# Show predicted measurement for remaining portion
            with gr.Row():
                with gr.Column():
                    z="filler"
                with gr.Column(elem_id="fit-box"):
                    remaining_str = gr.Textbox(label="📏 Remaining Portion Dimensions:", interactive=False, elem_id='pretty-box', visible=False)
                with gr.Column():
                    z="filler"

			# Title for top rim width
            with gr.Row():
                wt = gr.Markdown("# Width of Top part of Toilet:", elem_id="centered-title", visible=False)

			# Image showing top rim width
            with gr.Row():
                out_image = gr.Image(label="📏 Top Rim Width", show_share_button=False, show_fullscreen_button=False, height=600, width=800, visible=False, interactive=False, show_download_button=False, container=False)

			# Show top width measurement string
            with gr.Row():
                with gr.Column():
                    z="filler"
                with gr.Column(elem_id="fit-box"):
                    top_width_str = gr.Textbox(label="📏 Top Width:", interactive=False, elem_id='pretty-box', visible=False)
                with gr.Column():
                    z="filler"

			# Hidden file components for downloadable results or error files
            download_all_file = gr.File(visible=False)
            download_all_error = gr.File(visible=False)

			# Hidden session state holders (used for pipeline steps/status)
            res_stat1 = gr.State()
            step1 = gr.State()

			# Final result section (chassy, toilet, overlapped image and result text)
            with gr.Column():
                with gr.Row():
                    result_status1 = gr.Textbox(label="", visible=False, interactive=False, elem_id='pretty-box')
                with gr.Row():
                    with gr.Column():
                        result_image11 = gr.Image(label="Chassy Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False)
                    with gr.Column():
                        result_image21 = gr.Image(label="Toilet Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False)
                    with gr.Column():
                        result_image31 = gr.Image(label="Overlapped Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False)

			# Hidden column: entire error analysis and comparison UI
            with gr.Column(visible=False) as col:
				# Title
                gr.Markdown("## 🎯 Prediction Accuracy Analysis", elem_id="centered-title")

				# Subheader for reference diagram
                with gr.Row():
                    gr.Markdown("### 🖼️ Reference Diagram", elem_id="centered-title")
				
				# Static reference image for measurement guidance
                with gr.Row():
                    ref_image = gr.Image(value="./static/reference.jpg", interactive=False, label="Measurement Guide", show_share_button=False, show_fullscreen_button=False, height=600, show_download_button=False, container=False)

				# Unit selection (inches or cm)
                unit_dropdown = gr.Radio(choices=["in", "cm"], label="Select Unit", value="in")

				# Subheader for comparison table
                with gr.Row():
                    gr.Markdown("### 🧾 Predicted vs Actual Values Table", elem_id='centered-title')

				# Subheader for predicted vs actual plot
                with gr.Row():
                    gr.Markdown("### 📊 Predicted vs Actual Comparison")

                with gr.Column():
					# First row: input a, b, c, d (predicted and actual)
                    with gr.Row():
                        with gr.Column():
                            a1 = gr.Number(label="a (Predicted)", interactive=False)
                            a2 = gr.Number(label="a (Actual)")
                        with gr.Column():
                            b1 = gr.Number(label="b (Predicted)", interactive=False)
                            b2 = gr.Number(label="b (Actual)")
                    with gr.Row():
                        with gr.Column():
                            c1 = gr.Number(label="c (Predicted)", interactive=False)
                            c2 = gr.Number(label="c (Actual)")
                        with gr.Column():
                            d1 = gr.Number(label="d (Predicted)", interactive=False)
                            d2 = gr.Number(label="d (Actual)")

					# Second row: input e, f, g, h (predicted and actual)
                    with gr.Row():
                        with gr.Column():
                            e1 = gr.Number(label="e (Predicted)", interactive=False)
                            e2 = gr.Number(label="e (Actual)")
                        with gr.Column():
                            f1 = gr.Number(label="f (Predicted)", interactive=False)
                            f2 = gr.Number(label="f (Actual)")
                    with gr.Row():
                        with gr.Column():
                            g1 = gr.Number(label="g (Predicted)", interactive=False)
                            g2 = gr.Number(label="g (Actual)")
                        with gr.Column():
                            h1 = gr.Number(label="h (Predicted)", interactive=False)
                            h2 = gr.Number(label="h (Actual)")

				# Submit button to trigger error analysis
                submit_btn = gr.Button("🎯 Evaluate Accuracy")

				# Display progress/status of error evaluation
                with gr.Row():
                    progress = gr.Markdown("")

				# Hidden section for error result and visualization
                with gr.Column(visible=False) as error:
                    with gr.Row():
                        with gr.Column():
                            gr.Markdown("🧾 Individual Error %", elem_id='centered-title')
                            result_json = gr.JSON(label="🧾 Individual Error %")
                        with gr.Column():
                            gr.Markdown("📉 Error Plot", elem_id='centered-title')
                            error_plot = gr.Image(label="📉 Error Plot", height=400, width=600, interactive=False, visible=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False)
                    with gr.Row():
                        avg_error_text = gr.Textbox(label="🎯 Average Error %", interactive=False, elem_id='centered-title')

				# Hidden states to hold pipeline results and flow tracking
                res_stat = gr.State()
                step = gr.State()

				# Final output images and status (chassy, toilet, overlay)
                with gr.Column():
                    with gr.Row():
                        result_status = gr.Textbox(label="", visible=False, interactive=False, elem_id='pretty-box')
                    with gr.Row():
                        with gr.Column():
                            result_image1 = gr.Image(label="Chassy Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False)
                        with gr.Column():
                            result_image2 = gr.Image(label="Toilet Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False)
                        with gr.Column():
                            result_image3 = gr.Image(label="Overlapped Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False)

		# When Run Pipeline button is clicked: show the group section
        run_pipeline_btn.click(fn=lambda: gr.update(visible=True), outputs=group_to_show)

		# Update the processing message while running the pipeline
        run_pipeline_btn.click(fn=lambda: gr.update(value="🔄 Processing... (Please Wait)"), outputs=process)

		# When submit button is clicked: show calculation status
        submit_btn.click(fn=lambda: gr.update(value="🔄 Calculating Error Percentages... (Please Wait)"), outputs=progress)
            
        import gradio as gr

        # Function to compute predicted measurements based on selected unit
        def get_predictions_by_unit(unit, closed_remaining_in, top_to_hole_line_inch,
                                    hole_width_inch, out_rimellipse_inch, rim_height_inch, out_dist_in):
            # Compute each parameter in inches
            a1 = abs(closed_remaining_in - top_to_hole_line_inch)
            b1 = top_to_hole_line_inch
            c1 = hole_width_inch
            d1 = float(out_rimellipse_inch["right_inner"]) * 2  # Inner ellipse width
            e1 = float(out_rimellipse_inch["down_inner"]) * 2   # Inner ellipse height
            f1 = float(out_rimellipse_inch["right_outer"]) * 2  # Outer ellipse width
            g1 = rim_height_inch
            h1 = out_dist_in

            # If user selected inches, return raw inch values
            if unit == "in":
                return [round(a1, 2), round(b1, 2), round(c1, 2), round(d1, 2), round(e1, 2), round(f1, 2), round(g1, 2), round(h1, 2)] + [gr.update(value="✅ Process Complete.")]
            # If user selected cm, convert inch values to cm and return
            elif unit == "cm":
                return [round(x * 2.54, 2) for x in [a1, b1, c1, d1, e1, f1, g1, h1]] + [gr.update(value="✅ Process Complete.")]
            # If unknown unit, return default zeros
            else:
                return [0.0] * 8 + [gr.update(value="✅ Process Complete.")]

        # Function to compare predicted and ground truth measurements and compute percentage error
        def compare_measurements(a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2):
            # Convert all values to float (if they are passed as strings)
            a2, b2, c2, d2, e2, f2, g2, h2 = float(a2), float(b2), float(c2), float(d2), float(e2), float(f2), float(g2), float(h2)
            a1, b1, c1, d1, e1, f1, g1, h1 = float(a1), float(b1), float(c1), float(d1), float(e1), float(f1), float(g1), float(h1)

            # Helper function to compute error percentage
            def error(gt, pred):
                if gt == 0:
                    return "N/A"
                return f"{abs(gt - pred) / gt * 100:.2f}%"

            # Calculate individual errors for each parameter
            errors = {
                "a": error(a2, a1),
                "b": error(b2, b1),
                "c": error(c2, c1),
                "d": error(d2, d1),
                "e": error(e2, e1),
                "f": error(f2, f1),
                "g": error(g2, g1),
                "h": error(h2, h1)
            }

            # Compute valid numerical errors for average calculation
            valid_errors = [
                abs(gt - pred) / gt * 100
                for gt, pred in [
                    (a2, a1), (b2, b1), (c2, c1), (d2, d1), (e2, e1), (f2, f1), (g2, g1), (h2, h1)
                ] if gt != 0
            ]

            # Calculate average error across all valid parameters
            avg_error = f"{sum(valid_errors)/len(valid_errors):.2f}%" if valid_errors else "N/A"

            return errors, f"✅ Average Error: {avg_error}"

        # Wrapper function to update predictions whenever unit or inputs change
        def update_preds(unit, closed_remaining_in_p11, top_to_hole_line_inch_p10, hole_width_inch_p9, out_rimellipse_inch_p7, rim_height_inch_p8, out_dist_in):
            return get_predictions_by_unit(unit, closed_remaining_in_p11, top_to_hole_line_inch_p10,hole_width_inch_p9, out_rimellipse_inch_p7,rim_height_inch_p8, out_dist_in)

        # Set up the Gradio interaction: when dropdown changes, update prediction values
        unit_dropdown.change(
            fn=update_preds,
            inputs=[unit_dropdown, closed_remaining_in_p11, top_to_hole_line_inch_p10, hole_width_inch_p9, out_rimellipse_inch_p7, rim_height_inch_p8, out_dist_in],
            outputs=[a1, b1, c1, d1, e1, f1, g1, h1, process]
        )

        import matplotlib.pyplot as plt
        from PIL import Image
        import io

        # Function to generate error bar graph comparing predicted vs actual values
        def plot_error_graph(a2, b2, c2, d2, e2, f2, g2, h2, a1, b1, c1, d1, e1, f1, g1, h1):
            labels = list("abcdefgh")
            pred_vals = [float(x) for x in [a1, b1, c1, d1, e1, f1, g1, h1]]
            true_vals = [float(x) for x in [a2, b2, c2, d2, e2, f2, g2, h2]]

            # Calculate percentage error for each parameter
            errors = []
            for pred, true in zip(pred_vals, true_vals):
                if true == 0:
                    errors.append(0)
                else:
                    err = abs(pred - true) / true * 100
                    errors.append(err)

            # Plotting error bars
            fig, ax = plt.subplots(figsize=(8, 4))
            ax.bar(labels, errors, color="#f05a28")  # Use orange for bars
            ax.set_ylim(0, max(errors) * 1.2 if errors else 1)
            ax.set_ylabel("Error (%)")
            ax.set_xlabel("Parameter")
            ax.set_title("Individual Error per Parameter")

            # Convert Matplotlib plot to PIL image
            buf = io.BytesIO()
            plt.tight_layout()
            plt.savefig(buf, format='png')
            plt.close(fig)
            buf.seek(0)
            img = Image.open(buf)

            return gr.update(value=img, visible=True)

        # Function to download all results in a zipped file: image + text summary
        def download_all_results(a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2, avg_error_text, error_plot):
            # Skip download if no valid error
            if "N/A" in avg_error_text:
                return None, gr.update(value="")

            import os, zipfile, tempfile
            from PIL import Image

            # Create temporary directory to store files before zipping
            temp_dir = tempfile.mkdtemp()
            zip_path = os.path.join(tempfile.gettempdir(), "error_all_results.zip")

            with zipfile.ZipFile(zip_path, "w") as zipf:
                # Save images (only error plot for now)
                image_dict = {"error plot": error_plot}

                for name, img in image_dict.items():
                    if isinstance(img, np.ndarray):
                        img = Image.fromarray(img)
                    if isinstance(img, Image.Image):
                        img_path = os.path.join(temp_dir, f"{name}.png")
                        img.save(img_path)
                        zipf.write(img_path, arcname=f"{name}.png")

                # Save a detailed text summary of measurements
                text_lines = [
                    "📋 Measurement Summary",
                    "----------------------",
                    "📏 Predicted Values:",
                    f"a: {a1}",
                    f"b: {b1}",
                    f"c: {c1}",
                    f"d: {d1}",
                    f"e: {e1}",
                    f"f: {f1}",
                    f"g: {g1}",
                    f"h: {h1}",
                    "",
                    "📏 Actual Values:",
                    f"a: {a2}",
                    f"b: {b2}",
                    f"c: {c2}",
                    f"d: {d2}",
                    f"e: {e2}",
                    f"f: {f2}",
                    f"g: {g2}",
                    f"h: {h2}",
                    "",
                    "📏 Average Error:",
                    f"average error %: {avg_error_text}",
                ]
                summary_path = os.path.join(temp_dir, "error_analysis.txt")
                with open(summary_path, "w", encoding="utf-8") as f:
                    f.write("\n".join(text_lines))
                zipf.write(summary_path, arcname="error_analysis.txt")

            return zip_path, gr.update(value="")
        
        # Import necessary libraries
        import matplotlib.pyplot as plt             # For creating plots and figures
        import matplotlib.patches as patches        # For drawing polygons and ellipses on plot
        import numpy as np                          # For numerical operations (array min/max etc.)
        from PIL import Image                       # For image I/O with Pillow
        import os                                   # For interacting with the file system

        def draw_shapes_with_zorder(
            polygons_data=None,             # List of dictionaries representing polygons (each with 'points', 'facecolor', etc.)
            ellipses_data=None,             # List of dictionaries representing ellipses (each with 'center_x', 'width', etc.)
            res_stat=True,                  # Boolean to control whether to draw anything
            step=0,                         # Vertical shift step (multiplied by reference ratio)
            out_ref_ratios_p5=None,        # Dictionary with reference ratios, e.g., {'closed': 1.0}
            filename="chasis_image.png",   # Output filename for the saved figure
            fig_size=(18.1, 15),             # Size of the matplotlib figure
            transparent_bg=False           # Whether to make the saved image have transparent background
        ):
            # If result status is False, return None and skip drawing
            if not res_stat:
                return None
            
            # Compute vertical shift based on the 'step' and 'closed' reference ratio
            stepsize = step * out_ref_ratios_p5.get('closed', 1.0) if out_ref_ratios_p5 else 1.0
            
            # Create figure and axis for drawing
            fig, ax = plt.subplots(figsize=fig_size)
            ax.set_aspect('equal', adjustable='box')     # Maintain equal aspect ratio
            ax.set_axis_off()                            # Hide axes
            ax.set_facecolor('#f0f0f0')                  # Light gray background

            # Initialize bounding box tracking variables
            min_x, max_x = float('inf'), float('-inf')
            min_y, max_y = float('inf'), float('-inf')

            # List to store all coordinates for bounding box computation
            all_coords = []

            # ----------------- Draw Polygons -----------------
            if polygons_data:
                for poly_info in polygons_data:
                    points = poly_info.get('points')     # Get points of the polygon
                    if points:
                        # Apply vertical shift (stepsize) to all y-coordinates
                        shifted_points = [(x, y + stepsize) for x, y in points]
                        all_coords.extend(shifted_points)  # Store for bounds computation

                        # Create and style the polygon patch
                        polygon = patches.Polygon(
                            shifted_points,
                            closed=True,
                            facecolor=poly_info.get('facecolor', '#ADD8E6'),   # Default face color = light blue
                            edgecolor=poly_info.get('edgecolor', 'blue'),      # Default edge color = blue
                            linewidth=poly_info.get('linewidth', 2),           # Default border width
                            zorder=poly_info.get('zorder', 1)                  # Layering order
                        )
                        ax.add_patch(polygon)  # Add polygon to plot

            # ----------------- Draw Ellipses -----------------
            if ellipses_data:
                for ellipse_info in ellipses_data:
                    # Extract and apply vertical shift to ellipse center
                    e_cx = ellipse_info.get('center_x')
                    e_cy = ellipse_info.get('center_y') + stepsize
                    e_w = ellipse_info.get('width')
                    e_h = ellipse_info.get('height')
                    e_angle = ellipse_info.get('angle', 0)

                    # Skip if any critical ellipse parameter is missing
                    if e_cx is None or e_cy is None or e_w is None or e_h is None:
                        continue

                    # Add ellipse bounding box corners to coord list for computing limits
                    all_coords.append((e_cx - e_w / 2, e_cy - e_h / 2))
                    all_coords.append((e_cx + e_w / 2, e_cy + e_h / 2))
                    all_coords.append((e_cx - e_w / 2, e_cy + e_h / 2))
                    all_coords.append((e_cx + e_w / 2, e_cy - e_h / 2))

                    # Create and style the ellipse patch
                    ellipse = patches.Ellipse(
                        (e_cx, e_cy),
                        e_w,
                        e_h,
                        angle=e_angle,
                        facecolor=ellipse_info.get('facecolor', '#FFD700'),     # Default face color = gold
                        edgecolor=ellipse_info.get('edgecolor', 'orange'),      # Default edge color = orange
                        linewidth=ellipse_info.get('linewidth', 2),             # Default border width
                        zorder=ellipse_info.get('zorder', 1)                    # Layering order
                    )
                    ax.add_patch(ellipse)  # Add ellipse to plot

            # ----------------- Auto-fit Axis Limits -----------------
            if all_coords:
                coords_array = np.array(all_coords)                 # Convert list to NumPy array
                min_x, min_y = np.min(coords_array, axis=0)         # Find min x, y
                max_x, max_y = np.max(coords_array, axis=0)         # Find max x, y

                # Add 20% padding around bounding box
                padding_x = (max_x - min_x) * 0.2 if (max_x - min_x) > 0 else 1.0
                padding_y = (max_y - min_y) * 0.2 if (max_y - min_y) > 0 else 1.0

                # Set axis limits accordingly
                ax.set_xlim(min_x - padding_x, max_x + padding_x)
                ax.set_ylim(min_y - padding_y, max_y + padding_y)
            else:
                # Default axis range when no shapes present
                ax.set_xlim(0, 10)
                ax.set_ylim(0, 8)

            # ----------------- Save and Return Image -----------------
            plt.savefig(filename, dpi=300, bbox_inches='tight', pad_inches=0, transparent=transparent_bg)  # Save figure
            plt.close(fig)     # Always close the figure to avoid memory leaks

            res = Image.open(filename)  # Open saved image as PIL.Image
            return gr.update(visible=True, value=res)  # Return it in a Gradio-compatible format

        # State to store multiple polygon definitions
        my_polygons = gr.State([
            {
                'points': [(0.56, 14.72), (0.56, 19.05), (19.5, 19.05), (19.5, 14.53)],
                'facecolor': "#E6ADAD",     # Fill color
                'edgecolor': 'brown',       # Border color
                'linewidth': 0,             # No border line
                'zorder': 2                 # Drawing order
            },
            {
                'points': [(3, 14.72), (3, 9.45), (17, 9.45), (17, 14.6)],
                'facecolor': '#E6ADAD',
                'edgecolor': 'darkgreen',
                'linewidth': 0,
                'zorder': 1
            },
            {
                'points': [(4.24, 18.74), (9.68, 18.74), (9.68, 17.34), (4.24, 17.34)],
                'facecolor': '#f0f0f0',
                'edgecolor': 'brown',
                'linewidth': 0,
                'zorder': 4
            },
            {
                'points': [(12.25, 18.74), (15.88, 18.74), (15.88, 17.34), (12.25, 17.34)],
                'facecolor': '#f0f0f0',
                'edgecolor': 'darkgreen',
                'linewidth': 0,
                'zorder': 4
            }
        ])

        # State to store multiple ellipse definitions
        my_ellipses = gr.State([
            {
                'center_x': 10.0, 'center_y': 9.45, 'width': 9.0, 'height': 12.0, 'angle': 0,
                'facecolor': '#f0f0f0',
                'edgecolor': 'blue',
                'linewidth': 0,
                'zorder': 4
            },
            {
                'center_x': 10.0, 'center_y': 9.45, 'width': 14.0, 'height': 17.0, 'angle': 0,
                'facecolor': '#E6ADAD',
                'edgecolor': 'purple',
                'linewidth': 0,
                'zorder': 3
            }
        ])

        # Required imports
        import matplotlib.pyplot as plt
        import matplotlib.patches as patches
        import numpy as np
        from PIL import Image
        import os

        # =========================
        # Function to draw the custom shapes (polygons & ellipses) with vertical shift support
        # =========================
        def draw_shapes_with_zorder2(
            a,b,g,
            polygons_data=None,          # List of polygon specs
            ellipses_data=None,          # List of ellipse specs
            res_stat=True,               # Whether to proceed with drawing or not
            step=0,                      # Step used to calculate vertical shift
            out_ref_ratios_p5=None,      # Dictionary for px/cm conversion for scaling
            filename="toilet_image.png", # Output image file path
            title="Custom Shapes Drawing" # Plot title (unused but helpful for debug)
        ):
            if not res_stat:
                return None  # If drawing is disabled, return nothing

            # Setup figure and axes for plotting
            fig, ax = plt.subplots(figsize=(20, 15))
            ax.set_axis_off()  # Remove axis lines
            ax.set_aspect('equal', adjustable='box')  # Keep equal aspect ratio
            ax.set_facecolor('#f0f0f0')  # Background color

            # Determine vertical pixel shift from step using reference ratio
            ratio = out_ref_ratios_p5.get("closed", 1.0) if out_ref_ratios_p5 else 1.0
            vertical_shift = int(step * ratio)

            # Bounding box variables
            min_x, max_x = float('inf'), float('-inf')
            min_y, max_y = float('inf'), float('-inf')

            # Draw polygons if provided
            if polygons_data:
                for poly_info in polygons_data:
                    points = poly_info.get('points')
                    if not points:
                        continue  # Skip if no points defined

                    # Shift each point vertically
                    shifted_points = [(x, (y - vertical_shift)) for (x, y) in points]

                    # Update bounding box
                    for x, y in shifted_points:
                        min_x = min(min_x, x)
                        max_x = max(max_x, x)
                        min_y = min(min_y, y)
                        max_y = max(max_y, y)

                    # Create and add polygon patch
                    polygon = patches.Polygon(
                        shifted_points,
                        closed=True,
                        facecolor=poly_info.get('facecolor', '#ADD8E6'),
                        edgecolor=poly_info.get('edgecolor', 'blue'),
                        linewidth=poly_info.get('linewidth', 2),
                        label=poly_info.get('label', 'Polygon'),
                        zorder=poly_info.get('zorder', 1)
                    )
                    ax.add_patch(polygon)

            # Draw ellipses if provided
            if ellipses_data:
                for ellipse_info in ellipses_data:
                    e_cx = ellipse_info.get('center_x')
                    e_cy = ellipse_info.get('center_y')
                    e_w = ellipse_info.get('width')
                    e_h = ellipse_info.get('height')
                    e_angle = ellipse_info.get('angle', 0)

                    # Skip if required values are missing
                    if e_cx is None or e_cy is None or e_w is None or e_h is None:
                        continue

                    # Apply vertical shift
                    e_cy_shifted = e_cy - vertical_shift

                    # Update bounding box
                    min_x = min(min_x, e_cx - e_w / 2)
                    max_x = max(max_x, e_cx + e_w / 2)
                    min_y = min(min_y, e_cy_shifted - e_h / 2)
                    max_y = max(max_y, e_cy_shifted + e_h / 2)

                    # Create and add ellipse patch
                    ellipse = patches.Ellipse(
                        (e_cx, e_cy_shifted),
                        e_w,
                        e_h,
                        angle=e_angle,
                        facecolor=ellipse_info.get('facecolor', '#f0f0f0'),
                        edgecolor=ellipse_info.get('edgecolor', '#f0f0f0'),
                        linewidth=ellipse_info.get('linewidth', 2),
                        label=ellipse_info.get('label', 'Ellipse'),
                        zorder=ellipse_info.get('zorder', 1)
                    )
                    ax.add_patch(ellipse)

            # Adjust axis limits using computed bounding box
            if min_x != float('inf') and max_x != float('-inf'):
                padding_x = (max_x - min_x) * 0.2 if (max_x - min_x) > 0 else 1.0
                padding_y = (max_y - min_y) * 0.2 if (max_y - min_y) > 0 else 1.0
                ax.set_xlim(min_x - padding_x, max_x + padding_x)
                ax.set_ylim(min_y - padding_y, max_y + padding_y)
            else:
                # Default limits if nothing was drawn
                ax.set_xlim(0, 10)
                ax.set_ylim(0, 8)

            # Save the resulting image to disk
            plt.savefig(filename, dpi=300, bbox_inches='tight')
            plt.close(fig)

            # Load saved image into PIL and return for Gradio UI
            res = Image.open(filename)
            return gr.update(visible=True, value=res)
        
        # Import symbolic computation tools from sympy
        import sympy
        from sympy import symbols, Eq, solve, N

        # Function to find the points of tangency from an external point (px, py)
        # to an ellipse centered at (cx, cy) with axes lengths a and b
        def find_tangent_points_from_external(px, py, cx, cy, a, b):
            # Define symbolic variables
            x, y = symbols('x y')

            # Ellipse equation in canonical form
            ellipse_eq_sym = Eq((x - cx)**2 / a**2 + (y - cy)**2 / b**2, 1)

            # Equation for polar line from point (px, py) to the ellipse
            # This represents the tangent condition
            polar_eq_sym = Eq(
                (x - cx) * (px - cx) / a**2 +
                (y - cy) * (py - cy) / b**2, 1
            )

            # Solve the system of equations: ellipse + polar line
            solutions = solve([ellipse_eq_sym, polar_eq_sym], (x, y))

            tangent_points = []
            for sol in solutions:
                # Two types of solutions might be returned: dict or tuple

                if isinstance(sol, dict):
                    # If solution is a dictionary {x: val_x, y: val_y}
                    if all(s.is_real for s in sol.values()):
                        # Only accept real solutions
                        tangent_points.append((float(N(sol[x])), float(N(sol[y]))))

                elif isinstance(sol, tuple) and len(sol) == 2:
                    # If solution is a tuple (val_x, val_y)
                    if sol[0].is_real and sol[1].is_real:
                        tangent_points.append((float(N(sol[0])), float(N(sol[1]))))
            
            # Return the list of real tangent points
            return tangent_points

        # Wrapper function to compute one "left" and one "right" tangent point
        # from two external points to a given ellipse
        def get_specific_tangent_points(
            center_x, center_y, width, height,
            point1_x, point1_y, point2_x, point2_y
        ):
            # Convert width and height to ellipse semi-axes
            a = width / 2
            b = height / 2

            # Find all tangent points from external point 1
            tangent_points_p1 = find_tangent_points_from_external(point1_x, point1_y, center_x, center_y, a, b)

            # Find all tangent points from external point 2
            tangent_points_p2 = find_tangent_points_from_external(point2_x, point2_y, center_x, center_y, a, b)

            # --- Select the "left" tangent point for point 1 ---
            tangent_points_p1.sort(key=lambda p: p[0])  # Sort by x to pick leftmost
            selected_tangent_point_p1 = tangent_points_p1[0]

            # --- Select the "right" tangent point for point 2 ---
            tangent_points_p2.sort(key=lambda p: p[0])  # Sort by x to pick rightmost
            selected_tangent_point_p2 = tangent_points_p2[1]

            # Return the selected tangent points
            return selected_tangent_point_p2, selected_tangent_point_p1

        # --- Main function for drawing and guiding polygon visualization ---
        def guide(a2, b2, c2, d2, e2, f2, g2, h2, res_stat):
            # If toggle is off, return nothing
            if not res_stat:
                return None, None
            
            # Convert all inputs to float for computation
            a2, b2, c2, d2, e2, f2, g2, h2 = float(a2), float(b2), float(c2), float(d2), float(e2), float(f2), float(g2), float(h2)
            
            # Define fixed center for ellipse
            ellipse_center_x = 10
            ellipse_center_y = 10

            # Ellipse dimensions
            ellipse_width = f2   # major axis length (a*2)
            ellipse_height = g2  # minor axis length (b*2)

            # Define external points above the ellipse for tangents
            point_1_x, point_1_y = 10 - (h2/2), 10 + (e2/2) + a2 + b2
            point_2_x, point_2_y = 10 + (h2/2), 10 + (e2/2) + a2 + b2

            # Get the left and right tangent points
            tangent_points = get_specific_tangent_points(
                ellipse_center_x, ellipse_center_y,
                ellipse_width, ellipse_height,
                point_1_x, point_1_y, point_2_x, point_2_y
            )

            # Define polygon (irregular quadrilateral) from external and tangent points
            polygons = [
                {
                    'points': [
                        (10 - (h2/2), 10 + (e2/2) + a2 + b2),   # External point 1 (left)
                        (10 + (h2/2), 10 + (e2/2) + a2 + b2),   # External point 2 (right)
                        (tangent_points[0][0], tangent_points[0][1]),  # Right tangent
                        (tangent_points[1][0], tangent_points[1][1])   # Left tangent
                    ],
                    'facecolor': '#ADD8E6',  # Light blue fill
                    'edgecolor': 'brown',    # Border color
                    'linewidth': 0,          # No border line
                    'label': 'Irregular Quad',
                    'zorder': 2              # Above other shapes with lower zorder
                }
            ]

            # Define ellipse shapes to overlay on canvas
            ellipses = [
                {
                    'center_x': 10.0,
                    'center_y': 10,
                    'width': d2,             # Outer ellipse width
                    'height': e2,            # Outer ellipse height
                    'angle': 0,
                    'facecolor': '#f0f0f0',  # Very light gray
                    'edgecolor': 'blue',
                    'linewidth': 0,
                    'label': 'Rotated Ellipse',
                    'zorder': 4              # Top-most
                },
                {
                    'center_x': 10.0,
                    'center_y': 10,
                    'width': f2,             # Inner ellipse width
                    'height': g2,            # Inner ellipse height
                    'angle': 0,
                    'facecolor': '#ADD8E6',  # Light blue
                    'edgecolor': 'purple',
                    'linewidth': 0,
                    'label': 'Small Circle',
                    'zorder': 3
                },
                {
                    'center_x': 10 - (c2/2) + 0.75,
                    'center_y': 10 + (e2/2) + b2,
                    'width': 0.75,
                    'height': 0.75,
                    'angle': 0,
                    'facecolor': '#f0f0f0',
                    'edgecolor': 'blue',
                    'linewidth': 0,
                    'label': 'Rotated Ellipse',
                    'zorder': 4
                },
                {
                    'center_x': 10 + (c2/2) - 0.75,
                    'center_y': 10 + (e2/2) + b2,
                    'width': 0.75,
                    'height': 0.75,
                    'angle': 0,
                    'facecolor': '#f0f0f0',
                    'edgecolor': 'purple',
                    'linewidth': 0,
                    'label': 'Small Circle',
                    'zorder': 4
                }
            ]

            # Return both shapes: polygon and ellipses
            return polygons, ellipses
        
        from PIL import Image

        # Function to overlay two images (foreground onto background) with centering and opacity
        def overlay_images_centered(background_path, foreground_path, res_stat, step, out_ref_ratios_p5):
            if not res_stat:  # If product doesn't fit, skip overlay
                return None

            # Convert NumPy arrays to PIL Images and ensure they're in RGBA format
            bg = Image.fromarray(background_path).convert("RGBA")
            fg = Image.fromarray(foreground_path).convert("RGBA")

            # Make specific light gray color (#f0f0f0) transparent in the foreground
            new_data = [
                (255, 255, 255, 0) if pixel[:3] == (240, 240, 240) else pixel
                for pixel in fg.getdata()
            ]
            fg.putdata(new_data)

            # Get width and height of both images
            bg_w, bg_h = bg.size
            fg_w, fg_h = fg.size

            # Retrieve real-world px/cm ratio from previous part for accurate shift
            ratio = out_ref_ratios_p5.get("closed", 1.0)

            # Vertical shift is scaled by this ratio to maintain real-world measurement
            vertical_shift = int(step * ratio)

            # Create a transparent canvas same size as background
            shifted_fg = Image.new("RGBA", bg.size, (255, 255, 255, 0))

            # Calculate position to center the foreground and shift vertically upwards
            pos_x = (bg_w - fg_w) // 2
            pos_y = (bg_h - fg_h) // 2 - vertical_shift
            shifted_fg.paste(fg, (pos_x, pos_y), fg)  # Paste using alpha mask

            # Reduce opacity of foreground to 50% by adjusting the alpha channel
            alpha = shifted_fg.split()[3].point(lambda a: int(a * 0.5))
            shifted_fg.putalpha(alpha)

            # Overlay the semi-transparent foreground onto the background
            result = Image.alpha_composite(bg, shifted_fg).convert("RGB")

            # Return the final image and make it visible in the UI
            return gr.update(visible=True, value=result)

        # Logic to determine if product fits user's washroom based on input parameters
        def checkstatus(b2, c2, g2):
            hole_rad = 0.375  # Radius of the reference hole (in inches)
            max_range = 18.1  # Maximum allowed top clearance (inches)
            
            # Calculate top and bottom vertical bounds of fit area
            top_val = max_range - 0.31 - hole_rad
            bottom_val = top_val - 1.4 + hole_rad * 2

            # Minimum and maximum acceptable width values (inches)
            min_width = 4.5 + hole_rad * 2
            max_width = 11.52 - hole_rad * 2

            # Loop through step values from 0.0 to 0.5 in 0.05 increments
            max_step = 0.5
            step_size = 0.05
            steps = int(max_step / step_size) + 1  # Includes step=0

            for i in range(steps):
                step = i * step_size
                # Check if input values fall within allowed fit boundaries
                if float(bottom_val) < float(g2) + float(b2) + float(step) < float(top_val) and float(min_width) < float(c2) < float(max_width):
                    msg = "✅ Our Product can fit in your Washroom."
                    return gr.update(value=msg, visible=True), True, step

            # If not within any valid range, declare not fit
            msg = "❌ Unfortunately we cannot fit our product in your washroom."
            return gr.update(value=msg, visible=True), False, None

        # On submit: compare measurements, generate JSON, and plot error graph
        submit_btn.click(
            fn=lambda *args: (
                compare_measurements(*args)[0],  # JSON result
                compare_measurements(*args)[1],  # Average error text
                (plot_error_graph(*args))        # Error plot
            ),
            inputs=[a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2],
            outputs=[result_json, avg_error_text, error_plot]
        ).\
        then(fn=download_all_results,        # Then: prepare downloadable zip with results and update progress
            inputs=[a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2, avg_error_text, error_plot],
            outputs=[download_all_error, progress]).\
        then(checkstatus,        # Then: check if the product fits and determine optimal vertical step
            inputs=[b2, c2, g2], outputs=[result_status, res_stat, step]).\
        then(fn=draw_shapes_with_zorder,        # Then: draw background result image using input shape data
            inputs=[my_polygons, my_ellipses, res_stat, step, out_ref_ratios_p5], outputs=[result_image1]).\
        then(fn=guide,        # Then: generate guide overlay polygons and ellipses for fitting aid
            inputs=[a2, b2, c2, d2, e2, f2, g2, h2, res_stat], outputs=[polygons, ellipses]).\
        then(fn=draw_shapes_with_zorder2,        # Then: draw the guide shapes onto the second image
            inputs=[a2, b2, g2, polygons, ellipses, res_stat, step, out_ref_ratios_p5], outputs=[result_image2]).\
        then(fn=overlay_images_centered,        # Then: overlay guide image onto base result to create final preview
            inputs=[result_image1, result_image2, res_stat, step, out_ref_ratios_p5], outputs=[result_image3])

        # On submit, also reveal the general error message area in UI
        submit_btn.click(fn=lambda: gr.update(visible=True), inputs=[], outputs=[error])
        
        def download_all_results_combined(
            input1, input2, input3, 
            gallery_segmentation_p4, out_ref_image_p5, ref_ratios_str, 
            out_rim_image_p6, seat_measurement_str,
            out_rimellipse_image_p7, rim_measurement_str, 
            rimheight_image_p8, rim_height_str, 
            holewidth_image_p9, hole_width_str,
            rim_to_hole_img_p10, hole_to_top_str, 
            ellipse_viz_image_p10, direction_str, 
            rim_height_vis_p11, total_height_str,
            remaining_lid_img_p12, remaining_str, 
            out_image, top_width_str, 
            a1, b1, c1, d1, e1, f1, g1, h1
        ):
            import os, zipfile, tempfile
            from PIL import Image

            # Create a temporary directory to store intermediate image/text files
            temp_dir = tempfile.mkdtemp()

            # Define the path for the final zip file inside system temp directory
            zip_path = os.path.join(tempfile.gettempdir(), "all_results.zip")

            # Start creating a zip archive
            with zipfile.ZipFile(zip_path, "w") as zipf:

                # Dictionary mapping variable names to image objects (can be PIL or NumPy arrays)
                image_dict = {
                    "input1": input1,
                    "input2": input2,
                    "input3": input3,
                    "gallery_segmentation_p4": gallery_segmentation_p4,
                    "out_ref_image_p5": out_ref_image_p5,
                    "out_rim_image_p6": out_rim_image_p6,
                    "out_rimellipse_image_p7": out_rimellipse_image_p7,
                    "rimheight_image_p8": rimheight_image_p8,
                    "holewidth_image_p9": holewidth_image_p9,
                    "rim_to_hole_img_p10": rim_to_hole_img_p10,
                    "ellipse_viz_image_p10": ellipse_viz_image_p10,
                    "rim_height_vis_p11": rim_height_vis_p11,
                    "remaining_lid_img_p12": remaining_lid_img_p12,
                    "out_image": out_image,
                }

                # Loop through each image, save it as PNG if valid, and add it to the zip archive
                for name, img in image_dict.items():
                    if isinstance(img, np.ndarray):
                        img = Image.fromarray(img)  # Convert NumPy array to PIL Image if needed
                    if isinstance(img, Image.Image):  # Only process if it's a valid PIL Image
                        img_path = os.path.join(temp_dir, f"{name}.png")  # Temp image path
                        img.save(img_path)  # Save image to temp directory
                        zipf.write(img_path, arcname=f"{name}.png")  # Add to zip with name.png

                # Prepare a text summary with all the measurement-related strings and variables
                text_lines = [
                    "📋 Measurement Summary",
                    "----------------------",
                    f"ref_ratios_str:\n{ref_ratios_str}",  # Reference ratios from Part 5
                    f"seat_measurement_str:\n{seat_measurement_str}",  # Seat width (Part 6)
                    f"rim_measurement_str:\n{rim_measurement_str}",  # Rim ellipse (Part 7)
                    f"rim_height_str:\n{rim_height_str}",  # Rim height (Part 8)
                    f"hole_width_str:\n{hole_width_str}",  # Hole width (Part 9)
                    f"hole_to_top_str:\n{hole_to_top_str}",  # Rim to top (Part 10)
                    f"direction_str:\n{direction_str}",  # Direction analysis (Part 10)
                    f"total_height_str:\n{total_height_str}",  # Rim-to-ground height (Part 11)
                    f"remaining_str:\n{remaining_str}",  # Remaining lid space (Part 12)
                    f"top_width_str:\n{top_width_str}",  # Top width (Part 13)
                    "",
                    "📏 Predicted Values:",  # Model-predicted values
                    f"a: {a1}",
                    f"b: {b1}",
                    f"c: {c1}",
                    f"d: {d1}",
                    f"e: {e1}",
                    f"f: {f1}",
                    f"g: {g1}",
                    f"h: {h1}",
                    "",
                ]

                # Save the text summary into a .txt file inside the temporary directory
                summary_path = os.path.join(temp_dir, "summary.txt")
                with open(summary_path, "w", encoding="utf-8") as f:
                    f.write("\n".join(text_lines))  # Write summary contents line-by-line

                # Add the summary.txt file to the zip archive
                zipf.write(summary_path, arcname="summary.txt")

            # Return path to the created zip file and reset a Gradio state (usually download message)
            return zip_path, gr.update(value="")

        # 🔗 Pipeline chaining starting from Part 4
        run_pipeline_btn.click(fn=segment_and_overlay_all_p4,  # Step 1: Segment and overlay masks for all models
            inputs=[
                input1,            # Open seat image
                input2,            # Open no seat image
                input3,            # Closed lid image
                models_holes_p2,   # Hole model
                models_rim_p2,     # Rim model
                models_coinref_p2, # Coin/matchbox reference model
                device_p2          # Device: 'cuda' or 'cpu'
            ],
            outputs=[
                gallery_segmentation_p4,  # Gallery showing overlays
                binary_masks_p4,          # Segmentation masks
                image_dict_p4,            # Rotated/processed images
            ]).\
            then(fn=detect_and_plot_reference_p5,  # Step 2: Detect coin/matchbox and calculate px/cm ratios
            inputs=[
                image_dict_p4,
                binary_masks_p4
            ],
            outputs=[
                out_ref_image_p5,     # Overlay image with reference
                out_ref_ratios_p5,    # Dictionary with px/cm ratios
                ref_ratios_str,       # Stringified ratios for display
                ref                   # Raw reference size
            ]).\
            then(fn=analyze_rim_intersections_p6,  # Step 3: Measure open seat width (ellipse intersect)
            inputs=[
                image_dict_p4,
                binary_masks_p4,
                out_ref_ratios_p5
            ],
            outputs=[
                out_rim_image_p6,           # Image with measurement lines
                out_rim_measurements_p6,    # Raw measurements in px
                out_rim_measurements_cm_p6, # Converted to cm
                out_rim_measurements_inch_p6, # Converted to inches
                seat_measurement_str,       # Display string for seat
                seat                        # Measurement object
            ]).\
            then(fn=analyze_rim_intersections_p7,  # Step 4: Measure inner ellipse rim width
            inputs=[
                image_dict_p4,
                binary_masks_p4,
                out_ref_ratios_p5
            ],
            outputs=[
                out_rimellipse_image_p7,      # Visual result
                out_rimellipse_ui_p7,         # UI plot
                out_rimellipse_cm_p7,         # cm value
                out_rimellipse_inch_p7,       # inch value
                inner_top_p7,                 # Topmost point of inner rim
                dir_down_p7,                  # Direction vector (down)
                dir_right_p7,                 # Direction vector (right)
                rim_measurement_str,          # Display string
                rim                           # Measurement object
            ]).\
            then(fn=run_rim_height_analysis_p8,  # Step 5: Analyze rim height using open seat image
            inputs=[
                btn_rimheight_p8,    # Dummy trigger button
                image_dict_p4,
                binary_masks_p4,
                out_ref_ratios_p5,
                inner_top_p7,
                dir_down_p7
            ],
            outputs=[
                rimheight_text_p8,     # Textual explanation
                rimheight_image_p8,    # Annotated image
                rim_height_px_p8,      # px height
                rim_height_cm_p8,      # cm height
                rim_height_inch_p8,    # inch height
                inner_top_p8,          # Same as p7, but passed onward
                rim_height_str,        # Display string
                inlen                  # Measurement object
            ]).\
            then(fn=analyze_hole_width_perpendicular_p9,  # Step 6: Measure hole width perpendicular to rim
            inputs=[
                btn_measure_holewidth_p9,  # Dummy trigger
                binary_masks_p4,
                out_ref_ratios_p5,
                image_dict_p4,
                dir_right_p7,
                inner_top_p7
            ],
            outputs=[
                holewidth_image_p9,      # Annotated result
                hole_width_px_p9,
                hole_width_cm_p9,
                hole_width_inch_p9,
                angle_deg_p9,
                pt_min_p9,               # Leftmost point
                pt_max_p9,               # Rightmost point
                hole_width_str,
                hw
            ]).\
            then(fn=compute_top_to_hole_distance_p10,  # Step 7: Vertical distance between top rim and hole
            inputs=[
                btn_top_to_hole_p10,    # Trigger
                inner_top_p8,
                pt_min_p9,
                pt_max_p9,
                dir_down_p7,
                out_ref_ratios_p5,
                image_dict_p4
            ],
            outputs=[
                rim_to_hole_img_p10,            # Annotated image
                top_to_hole_line_px_p10,
                top_to_hole_line_cm_p10,
                top_to_hole_line_inch_p10,
                angle_down_deg_p10,
                angle_perp_deg_p10,
                intersection_point_p10,
                hole_to_top_str,
                hr
            ]).\
            then(fn=analyze_closed_rim_orientation_p10,  # Step 8: Fit ellipse on closed rim to get orientation
            inputs=[
                btn_ellipse_orient_p10,
                binary_masks_p4,
                image_dict_p4
            ],
            outputs=[
                ellipse_viz_image_p10,      # Ellipse fit result
                ellipse_angle_deg_p10,      # Angle of major axis
                ellipse_center_p10,
                ellipse_dir_down_p10,       # New downward vector
                ellipse_dir_right_p10,      # New rightward vector
                updated_binary_masks_p4,    # Updated with filtered contours
                direction_str,
                cl
            ]).\
            then(fn=analyze_rim_height_on_closed_p11,  # Step 9: Estimate full rim height using closed image
            inputs=[
                btn_rim_height_closed_p11,
                binary_masks_p4,
                input3,
                ellipse_angle_deg_p10,
                out_ref_ratios_p5,
                rim_height_cm_p8
            ],
            outputs=[
                rim_height_vis_p11,
                rim_height_cm_p11,
                rim_height_in_p11,
                full_rim_height_cm_p11,
                full_rim_height_in_p11,
                closed_remaining_cm_p11,
                closed_remaining_in_p11,
                pt_start_p11,
                total_height_str,
                th
            ]).\
            then(fn=draw_remaining_closed_portion_p12,  # Step 10: Draw estimated remaining closed portion
            inputs=[
                btn_draw_remaining_p12,
                pt_start_p11,
                closed_remaining_cm_p11,
                out_ref_ratios_p5,
                ellipse_dir_down_p10,
                input3
            ],
            outputs=[
                remaining_lid_img_p12,
                remaining_angle_deg_p12,
                pt_start_p12,
                pt_end_p12,
                remaining_str,
                tt
            ]).\
            then(fn=analyze_top_rim_width_p13,  # Step 11: Measure top closed rim width
            inputs=[
                btn_measure,
                binary_masks_p4,
                ellipse_center_p10,
                ellipse_angle_deg_p10,
                out_ref_ratios_p5,
                input3,
            ],
            outputs=[
                out_image,
                out_pt_top,
                out_pt1,
                out_pt2,
                out_dist_px,
                out_dist_cm,
                out_dist_in,
                out_angle_deg,
                top_width_str,
                wt
            ]).\
            then(fn=update_preds,  # Step 12: Compute final values and update UI
            inputs=[
                unit_dropdown,
                closed_remaining_in_p11,
                top_to_hole_line_inch_p10,
                hole_width_inch_p9,
                out_rimellipse_inch_p7,
                rim_height_inch_p8,
                out_dist_in
            ],
            outputs=[
                a1, b1, c1, d1, e1, f1, g1, h1, process
            ]).\
            then(fn=download_all_results_combined,  # Step 13: Prepare downloadable zip
            inputs=[
                input1, input2, input3,
                gallery_segmentation_p4,
                out_ref_image_p5, ref_ratios_str,
                out_rim_image_p6, seat_measurement_str,
                out_rimellipse_image_p7, rim_measurement_str,
                rimheight_image_p8, rim_height_str,
                holewidth_image_p9, hole_width_str,
                rim_to_hole_img_p10, hole_to_top_str,
                ellipse_viz_image_p10, direction_str,
                rim_height_vis_p11, total_height_str,
                remaining_lid_img_p12, remaining_str,
                out_image, top_width_str,
                a1, b1, c1, d1, e1, f1, g1, h1
            ],
            outputs=[download_all_file, progress]
            ).\
            then(checkstatus,  # Step 15: Verify that all required outputs are valid
            inputs=[b1, c1, g1],
            outputs=[result_status1, res_stat1, step1]
            ).\
            then(fn=draw_shapes_with_zorder,  # Step 16: Draw polygons/ellipses (step 1)
            inputs=[my_polygons, my_ellipses, res_stat1, step1, out_ref_ratios_p5],
            outputs=[result_image11]
            ).\
            then(fn=guide,  # Step 17: Generate additional shapes to draw (step 2)
            inputs=[a1, b1, c1, d1, e1, f1, g1, h1, res_stat1],
            outputs=[polygons1, ellipses1]
            ).\
            then(fn=draw_shapes_with_zorder2,  # Step 18: Draw second layer of shapes
            inputs=[a1, b1, g1, polygons1, ellipses1, res_stat1, step1, out_ref_ratios_p5],
            outputs=[result_image21]
            ).\
            then(fn=overlay_images_centered,  # Step 19: Final overlay combining result_image11 and 21
            inputs=[result_image11, result_image21, res_stat1, step1, out_ref_ratios_p5],
            outputs=[result_image31])

# Create a Gradio Blocks interface titled "🧪 Toilet Segmentation & Measurement App"
with gr.Blocks(title="🧪 Toilet Segmentation & Measurement App") as full_app_interface:
    
    # Define a Gradio State to store whether the user is authenticated or not
    #auth_state = gr.State(False)  # Initially set to False (not logged in)

    # ================================
    # 🔐 LOGIN SECTION
    # ================================
    # with gr.Column("🔐 Login") as login:
        # # Section Title
        # gr.Markdown("### Login", elem_id='centered-title')

        # # Email input row
        # with gr.Row():
            # with gr.Column():  # Left spacer
                # filler = ''
            # with gr.Column():  # Center column with email input
                # email = gr.Text(label="Email")
            # with gr.Column():  # Right spacer
                # filler = ''

        # # Password input row
        # with gr.Row():
            # with gr.Column():  # Left spacer
                # filler = ''
            # with gr.Column():  # Center column with password input
                # password = gr.Text(label="Password", type="password")
            # with gr.Column():  # Right spacer
                # filler = ''

        # # Login button row
        # with gr.Row():
            # with gr.Column():  # Left spacer
                # filler = ''
            # with gr.Column():  # Center column with Login button
                # login_btn = gr.Button("Login")
            # with gr.Column():  # Right spacer
                # filler = ''

        # # Authentication status message row
        # with gr.Row():
            # with gr.Column():  # Left spacer
                # filler = ''
            # with gr.Column():  # Center column with status message
                # auth_msg = gr.Textbox(label="Status", interactive=False)
            # with gr.Column():  # Right spacer
                # filler = ''

        # # Function to handle login logic
        # def handle_login(email, password):
            # # Call Supabase login helper with credentials
            # msg, success = supa_login(email, password)
            # # Return: message, show main app if success, update state, hide login if success
            # return msg, gr.update(visible=success), success, gr.update(visible=not success)

    # ================================
    # 🌐 MAIN APP SECTION (hidden by default)
    # ================================
    with gr.Column(visible=True) as protected_content:
        launch_main_app()  # Call to external function that builds the full app

        # # Attach login button to login handler
        # login_btn.click(
            # handle_login,
            # inputs=[email, password],
            # outputs=[auth_msg, protected_content, auth_state, login]
        # )

    # # Automatically toggle visibility of main app when authentication state changes
    # def toggle_app(auth):
        # return gr.update(visible=auth)

    # # Bind state change to toggle visibility
    # auth_state.change(toggle_app, inputs=auth_state, outputs=protected_content)

    # ================================
    # 🔻 FOOTER SECTION
    # ================================
    with gr.Row(elem_id='custom_footer'):
        with gr.Column():
            # Line 1 - Centered contact info
            gr.Markdown(" ")
            gr.Markdown(" ")
            gr.Markdown(
                "<div style='text-align:center;'>For any queries, feel free to contact 📧 <a href='mailto:core.atsc@gmail.com'>core.atsc@gmail.com</a></div>",
                elem_id="footer-contact"
            )

            # Line 2 - Left company info, Right creator info
            with gr.Row():
                with gr.Column(scale=1, min_width=300):
                    gr.Markdown(
                        "<div style='text-align:left;'>© 2025 HapiHygi Innovations Pvt. Ltd. &nbsp; All rights reserved.</div>",
                        elem_id="footer-left"
                    )
                with gr.Column(scale=1, min_width=300):
                    gr.Markdown(
                        """
                        <div style='text-align:right;'>
                            <a href="mailto:savaliyaheet19@gmail.com">👨‍💻</a>
                            <a href="https://github.com/heetsavaliya" target="_blank"><i>Created by:</i></a>
                            <a href="https://www.linkedin.com/in/heet-savaliya-03b863252/" target="_blank"><i>Heet Savaliya, PDEU</i></a>
                        </div>
                        """,
                        elem_id="footer-right"
                    )

# 🚀 Launch the full app
if __name__ == "__main__":
    full_app_interface.launch()