Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Pixel values are linear transmittance. The source corrections run in that domain
* **Stretch**: each channel stretches independently to $[0, 1]$, which removes the orange mask and base tints. The result is not clamped; the print curve's knees roll off tones outside the bounds.
* **Per-frame metering**: an exposure anchor (Auto Density) and a textural range ($P_{10}$ to $P_{90}$ luminance, Auto Grade), read on textured cells only. Sectors of 2×2 `activity_block` cells vote when their block means span more than `activity_gate_density` ($0.05$ log D; Boyack & Juenger, US 5,724,456); below `activity_min_fraction` passing sectors, every cell votes.
* **Channel unmix / crosstalk** (`crosstalk_strength` / `crosstalk_matrix`, docs/CROSSTALK.md): a 3×3 unmix of the negative log densities before bounds and stretch, where dye absorptions, light spectrum and CFA are linear, so a profile describes a scanning setup. It is blended with identity by strength and row-normalized, so grays stay. Every meter reads the unmixed film, so a change needs a new Roll Analysis.
* **Fade restoration** (`fade_ratio_r` / `fade_ratio_g` / `fade_ratio_b`, slides only: TRANSFER and POSITIVE): composes a diagonal restoration matrix $F^{-1}$ after the crosstalk unmix, $M = F^{-1} M_{\text{unmix}}$ (`features/process/fade.py::unmix_with_fade`). It runs before Cast Removal, which then meters the restored film. $F = \mathrm{diag}(a_r,\, a_r a_g,\, a_r a_b)$ acts on densities. $a_g$ and $a_b$ are survival ratios to red, which a neutral can constrain. $a_r$ is red's own survival and is set by eye. $F$ is the identity at unit survival, and it is not row-normalized.
* **Fade restoration** (`fade_ratio_r` / `fade_ratio_g` / `fade_ratio_b`, slides only: TRANSFER and POSITIVE): composes a diagonal restoration matrix $F^{-1}$ after the crosstalk unmix, $M = F^{-1} M_{\text{unmix}}$ (`features/process/fade.py::unmix_with_fade`). It runs before Cast Removal, which then meters the restored film. $F = \mathrm{diag}(a_r,\, a_r a_g,\, a_r a_b)$ acts on densities. $a_g$ and $a_b$ are survival ratios to red, which a neutral can constrain. $a_r$ is red's own survival, which no neutral constrains. $F$ is the identity at unit survival, and it is not row-normalized.

Estimate (`features/process/fade.py`) runs on request, never in a render. It renders the frame on the CPU with $F$ at identity and White Anchor off, and reads the transparency meter grid (`transfer_grid`: bakes, geometry, crop, camera matrix and unmix; a Positive source's grid stays in its own primaries). It meters the grays with `measure_neutral_axis_from_log` against the frame's own bounds: first with the foliage and sky sectors excluded, against bounds from each channel's extreme cells with decoded zeros dropped, at confidence `MIN_CONFIDENCE` ($0.2$) or more. Else it reads against bounds from the `ROBUST_EXTREME_PCT` ($0.1$) percentiles, with the sectors excluded, then not, at `ROBUST_MIN_CONFIDENCE` ($0.4$). MIDTONE reads the midtone densities and also sets $a_r$; SPREAD reads each channel's midtone-to-shadow spread. Ratios clamp to $[0.2, 5]$, and the hint shows the unclamped value. The hint also shows the other method's reading: a large gap is a cast that a fade does not explain. A near-monochrome slide meters as faded.

Expand Down
2 changes: 1 addition & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ Gives a faded slide its dye layers back.
* **Red Survival**: how much red dye is left. Lower it until the contrast comes back.
* **Green Survival** / **Blue Survival**: the same for green and blue, against red.
* **Remove Stain**: makes yellowed clear areas white, and runs Estimate. Try it when Estimate turns its icon amber.
* **Estimate**: sets the sliders from the grays in this frame. **Midtone** also sets Red Survival and suits a badly faded slide. **Gray Range** suits a mild fade with deep shadows.
* **Estimate**: sets the sliders from the grays in this frame. **Midtone** suits a badly faded slide. **Gray Range** suits a mild fade with deep shadows.

<!-- panel:process -->
### 10.5 Metering: negative → positive
Expand Down
14 changes: 4 additions & 10 deletions negpy/desktop/view/sidebar/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,8 @@ def _estimate_hint(method: "EstimateMethod", chosen: "FadeEstimate", other: "Fad
frame holds a cast the fade does not explain."""
if chosen.reason and chosen.green == 1.0 and chosen.blue == 1.0:
return chosen.reason
if chosen.red is not None:
text = tr("Estimated: red {r}, green {g}, blue {b}.", r=f"{chosen.red:.2f}", g=f"{chosen.green:.2f}", b=f"{chosen.blue:.2f}")
else:
text = tr(
"Estimated: green {g}, blue {b}. Red Survival is not changed: set it by eye.", g=f"{chosen.green:.2f}", b=f"{chosen.blue:.2f}"
)
red = chosen.red if chosen.red is not None else 1.0
text = tr("Estimated: red {r}, green {g}, blue {b}.", r=f"{red:.2f}", g=f"{chosen.green:.2f}", b=f"{chosen.blue:.2f}")
if chosen.reason:
text = chosen.reason + " " + text
if not (other.reason and other.green == 1.0 and other.blue == 1.0):
Expand Down Expand Up @@ -182,10 +178,8 @@ def _init_ui(self) -> None:
tr("How Estimate reads the grays: Midtone suits a badly faded slide, Gray Range a mild fade with deep shadows."),
[m.value for m in EstimateMethod],
)
self.estimate_method_combo.set_item_tooltip(0, tr("Midtone: from the color of the middle grays. It also sets Red Survival."))
self.estimate_method_combo.set_item_tooltip(
1, tr("Gray Range: from how far the grays reach into the shadows. It leaves Red Survival as it is.")
)
self.estimate_method_combo.set_item_tooltip(0, tr("Midtone: from the color of the middle grays."))
self.estimate_method_combo.set_item_tooltip(1, tr("Gray Range: from how far the grays reach into the shadows."))
saved = self.controller.session.repo.get_global_setting(_ESTIMATE_METHOD_KEY, EstimateMethod.MIDTONE)
self.estimate_method_combo.setCurrentIndex(
max(0, self.estimate_method_combo.findData(str(saved) if isinstance(saved, str) else ""))
Expand Down
6 changes: 3 additions & 3 deletions negpy/features/process/fade.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class EstimateMethod(StrEnum):

@dataclass(frozen=True)
class FadeEstimate:
"""Survivals from one Estimate. red is None when the method leaves Red Survival as it is;
"""Survivals from one Estimate. red is None when no estimate was made;
reason is "" for a real estimate (see fade_ratios_from_neutral_axis). white is the frame's
measured white, None when the frame is not a slide or has no cell to read it from."""

Expand Down Expand Up @@ -236,7 +236,7 @@ def fade_ratios_from_neutral_axis(refs: Optional[NeutralAxisRefs], method: str =
def estimate_fade(grid: ImageBuffer, white_anchor: bool = False) -> dict[EstimateMethod, FadeEstimate]:
"""Both methods' estimates from one measurement of the grays. `grid` is the transparency
meter's grid with the crosstalk unmix, no fade and no Remove Stain (`TransparencyBaseProcessor`).
MIDTONE also sets Red Survival so the best-kept layer lands at 1.0; a ratio above 1 says red
Both methods set Red Survival so the best-kept layer lands at 1.0; a ratio above 1 says red
faded most. With `white_anchor`, the grays are read as density above the frame's white, as
Remove Stain renders them."""
white = measure_white(grid)
Expand All @@ -250,7 +250,7 @@ def estimate_fade(grid: ImageBuffer, white_anchor: bool = False) -> dict[Estimat
continue
g, b, why = fade_ratios_from_neutral_axis(refs, method)
red = None
if method == EstimateMethod.MIDTONE and not (why and g == 1.0 and b == 1.0):
if not (why and g == 1.0 and b == 1.0):
lo, hi = RED_SURVIVAL_BOUNDS
red = min(max(1.0 / max(g, b, 1.0), lo), hi)
out[method] = FadeEstimate(red, g, b, why, white)
Expand Down
4 changes: 2 additions & 2 deletions negpy/translations/USER_GUIDE.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ Für eine Kamera mit Schmalbandlicht, die von jedem Bild drei Aufnahmen macht: R

Klicke mit der rechten Maustaste auf eine Hälfte für **Teilung für dieses Bild anpassen…** und **Teilung auf Rollenstandard zurücksetzen**. Wenn du Halbformat ausschaltest, bleiben bearbeitete Hälften als **Diptychon** nebeneinander. Schalte es wieder ein, um sie zu bearbeiten.

<!-- panel:sensor src:4f287366 -->
<!-- panel:sensor src:3a992073 -->
### 10.3 Kalibrierung: Farben deines Scan-Aufbaus

Korrigiert Farbfehler, die von Lichtquelle, Kamera und Film kommen.
Expand All @@ -363,7 +363,7 @@ Gibt einem ausgeblichenen Dia seine Farbstoffschichten zurück.
* **Rot-Erhalt**: wie viel roter Farbstoff übrig ist. Senke den Wert, bis der Kontrast zurückkommt.
* **Grün-Erhalt** / **Blau-Erhalt**: dasselbe für Grün und Blau, im Vergleich zu Rot.
* **Schleier entfernen**: macht gelb gewordene klare Bereiche weiß und führt Schätzen aus. Probiere es, wenn Schätzen sein Symbol gelb-orange färbt.
* **Schätzen**: stellt die Regler aus den Grautönen dieses Bildes ein. **Mitteltöne** stellt auch den Rot-Erhalt ein und passt zu einem stark ausgeblichenen Dia. **Grauumfang** passt zu einer leichten Ausbleichung mit tiefen Schatten.
* **Schätzen**: stellt die Regler aus den Grautönen dieses Bildes ein. **Mitteltöne** passt zu einem stark ausgeblichenen Dia. **Grauumfang** passt zu einer leichten Ausbleichung mit tiefen Schatten.

<!-- panel:process src:b1fdce87 -->
### 10.5 Messung: Negativ → Positiv
Expand Down
2 changes: 1 addition & 1 deletion negpy/translations/USER_GUIDE.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ Gives a faded slide its dye layers back.
* **Red Survival**: how much red dye is left. Lower it until the contrast comes back.
* **Green Survival** / **Blue Survival**: the same for green and blue, against red.
* **Remove Stain**: makes yellowed clear areas white, and runs Estimate. Try it when Estimate turns its icon amber.
* **Estimate**: sets the sliders from the grays in this frame. **Midtone** also sets Red Survival and suits a badly faded slide. **Gray Range** suits a mild fade with deep shadows.
* **Estimate**: sets the sliders from the grays in this frame. **Midtone** suits a badly faded slide. **Gray Range** suits a mild fade with deep shadows.

<!-- panel:process -->
### 10.5 Metering: negative → positive
Expand Down
4 changes: 2 additions & 2 deletions negpy/translations/USER_GUIDE.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室

フレームはロールに従う場合を除き自身のプロファイルを保持するため、光源やフィルムフォーマットごとにプロファイルを持てます。新しいフレームと、プロファイルが見つからないフレームは、最後に選んだプロファイルを使います。

<!-- panel:sensor src:4f287366 -->
<!-- panel:sensor src:3a992073 -->
## キャリブレーション:スキャン環境による色

光源、カメラ、フィルムから生じる色の誤差を補正します。
Expand All @@ -372,7 +372,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室
- **赤の残存率**:残っている赤の色素の量。コントラストが戻るまで下げます。
- **緑の残存率** / **青の残存率**:緑と青についての同じ値で、赤と比べた量です。
- **しみ除去**:黄色くなった透明な部分を白にし、推定を実行します。推定でアイコンがアンバー色になったら試してください。
- **推定**:このフレームのグレーからスライダーを設定します。**中間調**は赤の残存率も設定し、強く退色したスライドに向いています。**グレーの幅**は深いシャドウのある軽い退色に向いています。
- **推定**:このフレームのグレーからスライダーを設定します。**中間調**は強く退色したスライドに向いています。**グレーの幅**は深いシャドウのある軽い退色に向いています。

<!-- panel:process src:b1fdce87 -->
## 測光:ネガからポジへ
Expand Down
4 changes: 2 additions & 2 deletions negpy/translations/USER_GUIDE.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입

프레임은 롤을 따르지 않는 한 자체 프로필을 유지하므로 광원과 필름 포맷마다 프로필을 둘 수 있습니다. 새 프레임과 프로필이 없는 프레임은 마지막으로 고른 프로필을 사용합니다.

<!-- panel:sensor src:4f287366 -->
<!-- panel:sensor src:3a992073 -->
## 보정: 스캔 환경에 따른 색

광원, 카메라, 필름에서 생기는 색 오차를 보정합니다.
Expand All @@ -372,7 +372,7 @@ NegPy는 필름 스캔을 포지티브 이미지로 만드는 가상 암실입
- **빨강 잔존율**: 남아 있는 빨강 염료의 양입니다. 대비가 돌아올 때까지 낮추세요.
- **초록 잔존율** / **파랑 잔존율**: 초록과 파랑에 대한 같은 값으로, 빨강과 비교한 양입니다.
- **얼룩 제거**: 노랗게 변한 투명한 부분을 흰색으로 만들고 추정을 실행합니다. 추정 후 아이콘이 호박색이 되면 사용해 보세요.
- **추정**: 이 프레임의 회색으로 슬라이더를 설정합니다. **중간톤**은 빨강 잔존율도 설정하며 심하게 바랜 슬라이드에 맞습니다. **회색 범위**는 깊은 그림자가 있는 가벼운 퇴색에 맞습니다.
- **추정**: 이 프레임의 회색으로 슬라이더를 설정합니다. **중간톤**은 심하게 바랜 슬라이드에 맞습니다. **회색 범위**는 깊은 그림자가 있는 가벼운 퇴색에 맞습니다.

<!-- panel:process src:b1fdce87 -->
## 측광: 네거티브에서 포지티브로
Expand Down
4 changes: 2 additions & 2 deletions negpy/translations/USER_GUIDE.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样

除非帧跟随其胶卷,否则帧保留自己的配置,因此每种光源和胶片画幅都可以有一个配置。新帧或配置已缺失的帧使用你最后选择的配置。

<!-- panel:sensor src:4f287366 -->
<!-- panel:sensor src:3a992073 -->
## 校准:扫描设备带来的颜色

校正来自光源、相机和胶片的颜色误差。
Expand All @@ -372,7 +372,7 @@ NegPy 通过虚拟暗房将胶片扫描制作为正像。源图像保持原样
- **红色残留率**:剩余的红色染料量。降低它,直到反差恢复。
- **绿色残留率** / **蓝色残留率**:绿色和蓝色的相同值,相对红色而言。
- **去除污渍**:让变黄的透明区域变白,并运行估算。估算使其图标变为琥珀色时可以试用。
- **估算**:根据此画面中的灰色设置滑块。**中间调**同时设置红色残留率,适合严重褪色的反转片。**灰色范围**适合阴影较深的轻度褪色。
- **估算**:根据此画面中的灰色设置滑块。**中间调**适合严重褪色的反转片。**灰色范围**适合阴影较深的轻度褪色。

<!-- panel:process src:b1fdce87 -->
## 测光:从负片到正片
Expand Down
5 changes: 2 additions & 3 deletions negpy/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,6 @@
"Error: {text}": "Fehler: {text}",
"Estimate": "Schätzen",
"Estimate Fade Restoration from the grays in this frame": "Ausbleichrestaurierung aus den Grautönen dieses Bildes schätzen",
"Estimated: green {g}, blue {b}. Red Survival is not changed: set it by eye.": "Geschätzt: Grün {g}, Blau {b}. Rot-Erhalt bleibt unverändert: stelle ihn nach Augenmaß ein.",
"Estimated: red {r}, green {g}, blue {b}.": "Geschätzt: Rot {r}, Grün {g}, Blau {b}.",
"Even the fastest shutter with the LEDs at their minimum still clips the film base.": "Selbst die kürzeste Verschlusszeit bei minimaler LED-Helligkeit beschneidet noch die Filmbasis.",
"Even the slowest shutter with the LEDs at their maximum stays below the exposure target.": "Selbst die längste Verschlusszeit bei maximaler LED-Helligkeit bleibt unter dem Belichtungsziel.",
Expand Down Expand Up @@ -1146,7 +1145,7 @@
"Grain focuser loupe": "Kornschärfe-Lupe",
"Grain focuser: shows the exported pixels at the pointer at their true size, with a sharpness number to compare areas.": "Kornschärfe-Lupe: Zeigt die exportierten Pixel am Zeiger in Originalgröße, mit einem Schärfewert zum Vergleichen von Bereichen.",
"Gray Range": "Grauumfang",
"Gray Range: from how far the grays reach into the shadows. It leaves Red Survival as it is.": "Grauumfang: danach, wie weit die Grautöne in die Schatten reichen. Rot-Erhalt bleibt, wie er ist.",
"Gray Range: from how far the grays reach into the shadows.": "Grauumfang: danach, wie weit die Grautöne in die Schatten reichen.",
"Grayscale": "Graustufen",
"Green": "Grün",
"Green #58": "Grün #58",
Expand Down Expand Up @@ -1572,7 +1571,7 @@
"Mid Gray": "Mittelgrau",
"Midtone": "Mitteltöne",
"Midtone contrast, with no change to the deepest black and the paper white.": "Kontrast der Mitteltöne, ohne das tiefste Schwarz und das Papierweiß zu ändern.",
"Midtone: from the color of the middle grays. It also sets Red Survival.": "Mitteltöne: nach der Farbe der mittleren Grautöne. Stellt auch Rot-Erhalt ein.",
"Midtone: from the color of the middle grays.": "Mitteltöne: nach der Farbe der mittleren Grautöne.",
"Minimize": "Minimieren",
"Mode": "Modus",
"Mode: B&W Negative": "Modus: S/W-Negativ",
Expand Down
5 changes: 2 additions & 3 deletions negpy/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,6 @@
"Error: {text}": "エラー: {text}",
"Estimate": "推定",
"Estimate Fade Restoration from the grays in this frame": "このフレームのグレーから退色復元を推定",
"Estimated: green {g}, blue {b}. Red Survival is not changed: set it by eye.": "推定値:緑 {g}、青 {b}。赤の残存率は変わりません。目で見て設定してください。",
"Estimated: red {r}, green {g}, blue {b}.": "推定値:赤 {r}、緑 {g}、青 {b}。",
"Even the fastest shutter with the LEDs at their minimum still clips the film base.": "LEDを最小にして最速のシャッターを切ってもフィルムベースをクリップしてしまいます。",
"Even the slowest shutter with the LEDs at their maximum stays below the exposure target.": "LED を最大に設定した最も遅いシャッターでも、露出目標を下回ったままになります。",
Expand Down Expand Up @@ -1146,7 +1145,7 @@
"Grain focuser loupe": "グレインフォーカサールーペ",
"Grain focuser: shows the exported pixels at the pointer at their true size, with a sharpness number to compare areas.": "グレインフォーカサー: ポインター位置の書き出しピクセルを等倍で表示し、領域を比べるためのシャープネス値を示します。",
"Gray Range": "グレーの幅",
"Gray Range: from how far the grays reach into the shadows. It leaves Red Survival as it is.": "グレーの幅:グレーが影のどこまで届くかから推定します。赤の残存率はそのままです。",
"Gray Range: from how far the grays reach into the shadows.": "グレーの幅:グレーが影のどこまで届くかから推定します。",
"Grayscale": "グレースケール",
"Green": "緑",
"Green #58": "緑 #58",
Expand Down Expand Up @@ -1572,7 +1571,7 @@
"Mid Gray": "ミッドグレー",
"Midtone": "中間調",
"Midtone contrast, with no change to the deepest black and the paper white.": "中間調のコントラストで、最も深い黒と紙の白は変わりません。",
"Midtone: from the color of the middle grays. It also sets Red Survival.": "中間調:中間のグレーの色から推定します。赤の残存率も設定します。",
"Midtone: from the color of the middle grays.": "中間調:中間のグレーの色から推定します。",
"Minimize": "最小化する",
"Mode": "モード",
"Mode: B&W Negative": "モード: 白黒ネガ",
Expand Down
Loading
Loading